From e2956b87289a37747cb5431ad3a08dc202a2bcba Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 05:37:15 +1000
Subject: Set newest-first sorting for message deletion models, add log_url
property.
---
pydis_site/apps/api/models/bot/deleted_message.py | 4 ++--
pydis_site/apps/api/models/bot/message_deletion_context.py | 12 ++++++++++++
2 files changed, 14 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
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 44a0c8ae..02a15ca0 100644
--- a/pydis_site/apps/api/models/bot/message_deletion_context.py
+++ b/pydis_site/apps/api/models/bot/message_deletion_context.py
@@ -1,3 +1,4 @@
+from django.contrib.sites.models import Site
from django.db import models
from pydis_site.apps.api.models.bot.user import User
@@ -28,3 +29,14 @@ 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."""
+ domain = Site.objects.get_current().domain
+ return f"http://staff.{domain}/bot/logs/{self.id}/"
+
+ class Meta:
+ """Set the ordering for list views to newest first."""
+
+ ordering = ("-creation",)
--
cgit v1.2.3
From f590d2bc5960cafede1e596d67028052f2fad5b2 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 05:39:11 +1000
Subject: Add message log links, improved formatting to message deletion admin
pages.
---
pydis_site/apps/api/admin.py | 82 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 75 insertions(+), 7 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 059f52eb..6d6a9b3b 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -1,7 +1,10 @@
+import json
from typing import Optional
+from django import urls
from django.contrib import admin
from django.http import HttpRequest
+from django.utils.html import format_html
from .models import (
BotSetting,
@@ -44,21 +47,86 @@ 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(
+ "
{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
+ )
+
+
admin.site.register(BotSetting)
-admin.site.register(DeletedMessage)
+admin.site.register(DeletedMessage, DeletedMessageAdmin)
admin.site.register(DocumentationLink)
admin.site.register(Infraction)
admin.site.register(LogEntry, LogEntryAdmin)
-admin.site.register(MessageDeletionContext)
+admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin)
admin.site.register(Nomination)
admin.site.register(OffTopicChannelName)
admin.site.register(Role)
--
cgit v1.2.3
From 618610fe367b0c8d6175d251c276c2e37db8aa52 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 05:43:21 +1000
Subject: Order roles by positioning, add filters and search to api user admin
page.
---
pydis_site/apps/api/admin.py | 45 ++++++++++++++++++++++++++++++++--
pydis_site/apps/api/models/bot/role.py | 5 ++++
2 files changed, 48 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 6d6a9b3b..65cc0a6c 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -1,8 +1,9 @@
import json
-from typing import Optional
+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
@@ -121,6 +122,46 @@ class MessageDeletionContextAdmin(admin.ModelAdmin):
)
+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)
@@ -131,4 +172,4 @@ admin.site.register(Nomination)
admin.site.register(OffTopicChannelName)
admin.site.register(Role)
admin.site.register(Tag)
-admin.site.register(User)
+admin.site.register(User, UserAdmin)
diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py
index 58bbf8b4..b95740da 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",)
--
cgit v1.2.3
From 0021d4a2906021351203ffabc73d6e02ecf400c4 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 06:21:13 +1000
Subject: Improve infractions admin list and page, add search and filters.
---
pydis_site/apps/api/admin.py | 46 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 45 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 65cc0a6c..74b9413b 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -122,6 +122,50 @@ class MessageDeletionContextAdmin(admin.ModelAdmin):
)
+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 StaffRolesFilter(admin.SimpleListFilter):
"""Filter options for Staff Roles."""
@@ -165,7 +209,7 @@ class UserAdmin(admin.ModelAdmin):
admin.site.register(BotSetting)
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, MessageDeletionContextAdmin)
admin.site.register(Nomination)
--
cgit v1.2.3
From 1c41c8a1aa07d7a716561c7594f769db7aa58cf5 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 06:33:35 +1000
Subject: Improve nominations admin list and page, add search and filter by
active.
---
pydis_site/apps/api/admin.py | 39 +++++++++++++++++++++++++++-
pydis_site/apps/api/models/bot/nomination.py | 5 ++++
2 files changed, 43 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 74b9413b..55a9d655 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -166,6 +166,43 @@ class InfractionAdmin(admin.ModelAdmin):
)
+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 StaffRolesFilter(admin.SimpleListFilter):
"""Filter options for Staff Roles."""
@@ -212,7 +249,7 @@ admin.site.register(DocumentationLink)
admin.site.register(Infraction, InfractionAdmin)
admin.site.register(LogEntry, LogEntryAdmin)
admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin)
-admin.site.register(Nomination)
+admin.site.register(Nomination, NominationAdmin)
admin.site.register(OffTopicChannelName)
admin.site.register(Role)
admin.site.register(Tag)
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index cd9951aa..a0ba42a3 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -44,3 +44,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",)
--
cgit v1.2.3
From 77da3336a248a5a01fe1f83493e0424dbd261cd2 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 06:37:55 +1000
Subject: Add search field to off topic admin page.
---
pydis_site/apps/api/admin.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 55a9d655..fe0e3235 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -203,6 +203,12 @@ class NominationAdmin(admin.ModelAdmin):
list_filter = ("active",)
+class OffTopicChannelNameAdmin(admin.ModelAdmin):
+ """Admin formatting for the OffTopicChannelName model."""
+
+ search_fields = ("name",)
+
+
class StaffRolesFilter(admin.SimpleListFilter):
"""Filter options for Staff Roles."""
@@ -250,7 +256,7 @@ admin.site.register(Infraction, InfractionAdmin)
admin.site.register(LogEntry, LogEntryAdmin)
admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin)
admin.site.register(Nomination, NominationAdmin)
-admin.site.register(OffTopicChannelName)
+admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin)
admin.site.register(Role)
admin.site.register(Tag)
admin.site.register(User, UserAdmin)
--
cgit v1.2.3
From 72f156fda828a7e28605d0fe07bd990d05f61925 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 07:18:42 +1000
Subject: Show role colour style and add hex value, link perms to calc page,
add role search.
---
pydis_site/apps/api/admin.py | 34 +++++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index fe0e3235..237d68a4 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -209,6 +209,38 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin):
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 StaffRolesFilter(admin.SimpleListFilter):
"""Filter options for Staff Roles."""
@@ -257,6 +289,6 @@ admin.site.register(LogEntry, LogEntryAdmin)
admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin)
admin.site.register(Nomination, NominationAdmin)
admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin)
-admin.site.register(Role)
+admin.site.register(Role, RoleAdmin)
admin.site.register(Tag)
admin.site.register(User, UserAdmin)
--
cgit v1.2.3
From e19d34b06f6485d2809b600547ae5b672c31fe7e Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 07:50:00 +1000
Subject: Add tag search and rendered preview.
---
pydis_site/apps/api/admin.py | 28 ++++++++++++++++++++++++++--
1 file changed, 26 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 237d68a4..010541a6 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -82,7 +82,8 @@ class DeletedMessageAdmin(admin.ModelAdmin):
"""Format embed data in a code block for better readability."""
if instance.embeds:
return format_html(
- "{0}
",
+ ""
+ "{0}
",
json.dumps(instance.embeds, indent=4)
)
@@ -241,6 +242,29 @@ class RoleAdmin(admin.ModelAdmin):
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."""
@@ -290,5 +314,5 @@ admin.site.register(MessageDeletionContext, MessageDeletionContextAdmin)
admin.site.register(Nomination, NominationAdmin)
admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin)
admin.site.register(Role, RoleAdmin)
-admin.site.register(Tag)
+admin.site.register(Tag, TagAdmin)
admin.site.register(User, UserAdmin)
--
cgit v1.2.3
From b6d01478bab4a2706202d44f117498ca676c00df Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 08:51:28 +1000
Subject: Adjust deleted message test to account for new ordering of newest
created first.
---
pydis_site/apps/staff/tests/test_logs_view.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py
index 32cb6bbf..5036363b 100644
--- a/pydis_site/apps/staff/tests/test_logs_view.py
+++ b/pydis_site/apps/staff/tests/test_logs_view.py
@@ -132,7 +132,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())
)
--
cgit v1.2.3
From 56c6c85ad973a3b594d72215cdfb2d3b6c19d741 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Oct 2019 09:29:58 +1000
Subject: Add new test for deleted message context log_url.
---
.../api/models/bot/message_deletion_context.py | 5 ++---
pydis_site/apps/api/tests/test_deleted_messages.py | 22 ++++++++++++++++++++++
2 files changed, 24 insertions(+), 3 deletions(-)
(limited to 'pydis_site')
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 02a15ca0..fde9b0a6 100644
--- a/pydis_site/apps/api/models/bot/message_deletion_context.py
+++ b/pydis_site/apps/api/models/bot/message_deletion_context.py
@@ -1,5 +1,5 @@
-from django.contrib.sites.models import Site
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.utils import ModelReprMixin
@@ -33,8 +33,7 @@ class MessageDeletionContext(ModelReprMixin, models.Model):
@property
def log_url(self) -> str:
"""Create the url for the deleted message logs."""
- domain = Site.objects.get_current().domain
- return f"http://staff.{domain}/bot/logs/{self.id}/"
+ return reverse('logs', host="staff", args=(self.id,))
class Meta:
"""Set the ordering for list views to newest first."""
diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py
index d1e9f2f5..ccccdda4 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
@@ -75,3 +76,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)
--
cgit v1.2.3
From 1a55e92824d56a34a677c1f764a5393038e66955 Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Sat, 22 Aug 2020 01:40:55 +0200
Subject: Add a context processor that outputs the git SHA.
This will make the SHA available in all templates.
---
pydis_site/context_processors.py | 10 ++++++++++
pydis_site/settings.py | 2 +-
2 files changed, 11 insertions(+), 1 deletion(-)
create mode 100644 pydis_site/context_processors.py
(limited to 'pydis_site')
diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py
new file mode 100644
index 00000000..4ae0dbb3
--- /dev/null
+++ b/pydis_site/context_processors.py
@@ -0,0 +1,10 @@
+import git
+from django.template import RequestContext
+
+REPO = git.Repo(search_parent_directories=True)
+SHA = REPO.head.object.hexsha
+
+
+def git_sha_processor(_: RequestContext) -> dict:
+ """Expose the git SHA for this repo to all views."""
+ return {'git_sha': SHA}
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 2c87007c..afa097be 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -157,8 +157,8 @@ TEMPLATES = [
'django.template.context_processors.static',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
-
"sekizai.context_processors.sekizai",
+ "pydis_site.context_processors.git_sha_processor"
],
},
},
--
cgit v1.2.3
From 75181a55977abe2eb6f3ac782b51bf13b8945024 Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Sat, 22 Aug 2020 01:41:20 +0200
Subject: Add a comment with the git SHA to base templates.
---
pydis_site/templates/base/base.html | 1 +
pydis_site/templates/wiki/base.html | 1 +
2 files changed, 2 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html
index 4c70d778..70426dc1 100644
--- a/pydis_site/templates/base/base.html
+++ b/pydis_site/templates/base/base.html
@@ -37,6 +37,7 @@
{% render_block "css" %}
+
{% if messages %}
diff --git a/pydis_site/templates/wiki/base.html b/pydis_site/templates/wiki/base.html
index 846492ab..c9faf914 100644
--- a/pydis_site/templates/wiki/base.html
+++ b/pydis_site/templates/wiki/base.html
@@ -19,6 +19,7 @@
{% endblock %}
{% block content %}
+
{% block site_navbar %}
{% include "base/navbar.html" %}
{% endblock %}
--
cgit v1.2.3
From 60bc529e4b1cfa59e82ede102b88fca08f8f93b4 Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Sat, 22 Aug 2020 02:04:44 +0200
Subject: Move git SHA initialization to __init__.py.
This will make it easier to use in multiple places.
---
pydis_site/__init__.py | 5 +++++
pydis_site/context_processors.py | 6 ++----
2 files changed, 7 insertions(+), 4 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py
index df67cf71..f0702577 100644
--- a/pydis_site/__init__.py
+++ b/pydis_site/__init__.py
@@ -1,3 +1,4 @@
+import git
from wiki.plugins.macros.mdx import toc
# Remove the toc header prefix. There's no option for this, so we gotta monkey patch it.
@@ -7,3 +8,7 @@ toc.HEADER_ID_PREFIX = ''
# by a string because Allauth won't let us just give it a list _there_, we have to point
# at a list _somewhere else_ instead.
VALIDATORS = []
+
+# Git SHA
+repo = git.Repo(search_parent_directories=True)
+GIT_SHA = repo.head.object.hexsha
diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py
index 4ae0dbb3..bb66f21d 100644
--- a/pydis_site/context_processors.py
+++ b/pydis_site/context_processors.py
@@ -1,10 +1,8 @@
-import git
from django.template import RequestContext
-REPO = git.Repo(search_parent_directories=True)
-SHA = REPO.head.object.hexsha
+from pydis_site import GIT_SHA
def git_sha_processor(_: RequestContext) -> dict:
"""Expose the git SHA for this repo to all views."""
- return {'git_sha': SHA}
+ return {'git_sha': GIT_SHA}
--
cgit v1.2.3
From 212a06f400bfce95281e4c8d11baf6cb09e70861 Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Sat, 22 Aug 2020 02:05:24 +0200
Subject: Set the sentry_sdk.init release to git_sha.
This is the first step in implementing releases for Sentry.
---
pydis_site/settings.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index afa097be..ff2942f0 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -20,6 +20,7 @@ import sentry_sdk
from django.contrib.messages import constants as messages
from sentry_sdk.integrations.django import DjangoIntegration
+from pydis_site import GIT_SHA
if typing.TYPE_CHECKING:
from django.contrib.auth.models import User
@@ -33,7 +34,8 @@ env = environ.Env(
sentry_sdk.init(
dsn=env('SITE_SENTRY_DSN'),
integrations=[DjangoIntegration()],
- send_default_pii=True
+ send_default_pii=True,
+ release=f"pydis-site@{GIT_SHA}"
)
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
--
cgit v1.2.3
From 416a0874187fdf96346e3f504d6542b3214957db Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Sat, 22 Aug 2020 02:49:28 +0200
Subject: Move git SHA fetcher into utils.
---
pydis_site/__init__.py | 5 -----
pydis_site/context_processors.py | 4 ++--
pydis_site/settings.py | 4 ++--
pydis_site/utils/resources.py | 10 ++++++++++
4 files changed, 14 insertions(+), 9 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py
index f0702577..df67cf71 100644
--- a/pydis_site/__init__.py
+++ b/pydis_site/__init__.py
@@ -1,4 +1,3 @@
-import git
from wiki.plugins.macros.mdx import toc
# Remove the toc header prefix. There's no option for this, so we gotta monkey patch it.
@@ -8,7 +7,3 @@ toc.HEADER_ID_PREFIX = ''
# by a string because Allauth won't let us just give it a list _there_, we have to point
# at a list _somewhere else_ instead.
VALIDATORS = []
-
-# Git SHA
-repo = git.Repo(search_parent_directories=True)
-GIT_SHA = repo.head.object.hexsha
diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py
index bb66f21d..e905d9c7 100644
--- a/pydis_site/context_processors.py
+++ b/pydis_site/context_processors.py
@@ -1,8 +1,8 @@
from django.template import RequestContext
-from pydis_site import GIT_SHA
+from pydis_site.utils.resources import get_git_sha
def git_sha_processor(_: RequestContext) -> dict:
"""Expose the git SHA for this repo to all views."""
- return {'git_sha': GIT_SHA}
+ return {'git_sha': get_git_sha()}
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index ff2942f0..e707a526 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -20,7 +20,7 @@ import sentry_sdk
from django.contrib.messages import constants as messages
from sentry_sdk.integrations.django import DjangoIntegration
-from pydis_site import GIT_SHA
+from pydis_site.utils.resources import get_git_sha
if typing.TYPE_CHECKING:
from django.contrib.auth.models import User
@@ -35,7 +35,7 @@ sentry_sdk.init(
dsn=env('SITE_SENTRY_DSN'),
integrations=[DjangoIntegration()],
send_default_pii=True,
- release=f"pydis-site@{GIT_SHA}"
+ release=f"pydis-site@{get_git_sha()}"
)
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
diff --git a/pydis_site/utils/resources.py b/pydis_site/utils/resources.py
index 637fd785..d36c4b77 100644
--- a/pydis_site/utils/resources.py
+++ b/pydis_site/utils/resources.py
@@ -4,8 +4,13 @@ import glob
import typing
from dataclasses import dataclass
+import git
import yaml
+# Git SHA
+repo = git.Repo(search_parent_directories=True)
+GIT_SHA = repo.head.object.hexsha
+
@dataclass
class URL:
@@ -89,3 +94,8 @@ def load_categories(order: typing.List[str]) -> typing.List[Category]:
categories.append(Category.construct_from_directory(direc))
return categories
+
+
+def get_git_sha() -> str:
+ """Get the Git SHA for this repo."""
+ return GIT_SHA
--
cgit v1.2.3
From 503bfba6855d3019a2739bb1adce69c457a8b26e Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Sat, 22 Aug 2020 03:18:25 +0200
Subject: Move git SHA fetcher into its own file.
Fix tests.
---
pydis_site/context_processors.py | 2 +-
pydis_site/settings.py | 2 +-
pydis_site/tests/test_utils.py | 11 +++++++++++
pydis_site/utils/__init__.py | 3 +++
pydis_site/utils/resources.py | 10 ----------
pydis_site/utils/utils.py | 10 ++++++++++
6 files changed, 26 insertions(+), 12 deletions(-)
create mode 100644 pydis_site/tests/test_utils.py
create mode 100644 pydis_site/utils/__init__.py
create mode 100644 pydis_site/utils/utils.py
(limited to 'pydis_site')
diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py
index e905d9c7..ab5a4168 100644
--- a/pydis_site/context_processors.py
+++ b/pydis_site/context_processors.py
@@ -1,6 +1,6 @@
from django.template import RequestContext
-from pydis_site.utils.resources import get_git_sha
+from pydis_site.utils import get_git_sha
def git_sha_processor(_: RequestContext) -> dict:
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index e707a526..0a5b0eed 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -20,7 +20,7 @@ import sentry_sdk
from django.contrib.messages import constants as messages
from sentry_sdk.integrations.django import DjangoIntegration
-from pydis_site.utils.resources import get_git_sha
+from pydis_site.utils import get_git_sha
if typing.TYPE_CHECKING:
from django.contrib.auth.models import User
diff --git a/pydis_site/tests/test_utils.py b/pydis_site/tests/test_utils.py
new file mode 100644
index 00000000..f1419860
--- /dev/null
+++ b/pydis_site/tests/test_utils.py
@@ -0,0 +1,11 @@
+from django.test import TestCase
+
+from pydis_site.utils import get_git_sha
+from pydis_site.utils.utils import GIT_SHA
+
+
+class UtilsTests(TestCase):
+
+ def test_git_sha(self):
+ """Test that the get_git_sha returns the correct SHA."""
+ self.assertEqual(get_git_sha(), GIT_SHA)
diff --git a/pydis_site/utils/__init__.py b/pydis_site/utils/__init__.py
new file mode 100644
index 00000000..bb91b3d8
--- /dev/null
+++ b/pydis_site/utils/__init__.py
@@ -0,0 +1,3 @@
+from .utils import get_git_sha
+
+__all__ = ['get_git_sha']
diff --git a/pydis_site/utils/resources.py b/pydis_site/utils/resources.py
index d36c4b77..637fd785 100644
--- a/pydis_site/utils/resources.py
+++ b/pydis_site/utils/resources.py
@@ -4,13 +4,8 @@ import glob
import typing
from dataclasses import dataclass
-import git
import yaml
-# Git SHA
-repo = git.Repo(search_parent_directories=True)
-GIT_SHA = repo.head.object.hexsha
-
@dataclass
class URL:
@@ -94,8 +89,3 @@ def load_categories(order: typing.List[str]) -> typing.List[Category]:
categories.append(Category.construct_from_directory(direc))
return categories
-
-
-def get_git_sha() -> str:
- """Get the Git SHA for this repo."""
- return GIT_SHA
diff --git a/pydis_site/utils/utils.py b/pydis_site/utils/utils.py
new file mode 100644
index 00000000..2033ea19
--- /dev/null
+++ b/pydis_site/utils/utils.py
@@ -0,0 +1,10 @@
+import git
+
+# Git SHA
+repo = git.Repo(search_parent_directories=True)
+GIT_SHA = repo.head.object.hexsha
+
+
+def get_git_sha() -> str:
+ """Get the Git SHA for this repo."""
+ return GIT_SHA
--
cgit v1.2.3
From 88d364bfc809d20c6c4645abba75b889a526c804 Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Sat, 22 Aug 2020 03:46:08 +0200
Subject: Remove the SHA from the wiki base.html
This extends the regular base.html, so this would cause wiki pages to
have two SHA's.
---
pydis_site/templates/wiki/base.html | 1 -
1 file changed, 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/templates/wiki/base.html b/pydis_site/templates/wiki/base.html
index c9faf914..846492ab 100644
--- a/pydis_site/templates/wiki/base.html
+++ b/pydis_site/templates/wiki/base.html
@@ -19,7 +19,6 @@
{% endblock %}
{% block content %}
-
{% block site_navbar %}
{% include "base/navbar.html" %}
{% endblock %}
--
cgit v1.2.3
From a71c38f07bba0191ba28eef12eea56e8a9265349 Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Sat, 22 Aug 2020 03:54:51 +0200
Subject: Move the SHA into constants.py.
The util was redundant. Thanks @MarkKoz
---
pydis_site/constants.py | 5 +++++
pydis_site/context_processors.py | 4 ++--
pydis_site/settings.py | 4 ++--
pydis_site/tests/test_utils.py | 11 -----------
pydis_site/utils/__init__.py | 3 ---
pydis_site/utils/utils.py | 10 ----------
6 files changed, 9 insertions(+), 28 deletions(-)
create mode 100644 pydis_site/constants.py
delete mode 100644 pydis_site/tests/test_utils.py
delete mode 100644 pydis_site/utils/__init__.py
delete mode 100644 pydis_site/utils/utils.py
(limited to 'pydis_site')
diff --git a/pydis_site/constants.py b/pydis_site/constants.py
new file mode 100644
index 00000000..0b76694a
--- /dev/null
+++ b/pydis_site/constants.py
@@ -0,0 +1,5 @@
+import git
+
+# Git SHA
+repo = git.Repo(search_parent_directories=True)
+GIT_SHA = repo.head.object.hexsha
diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py
index ab5a4168..6937a3db 100644
--- a/pydis_site/context_processors.py
+++ b/pydis_site/context_processors.py
@@ -1,8 +1,8 @@
from django.template import RequestContext
-from pydis_site.utils import get_git_sha
+from pydis_site.constants import GIT_SHA
def git_sha_processor(_: RequestContext) -> dict:
"""Expose the git SHA for this repo to all views."""
- return {'git_sha': get_git_sha()}
+ return {'git_sha': GIT_SHA}
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 0a5b0eed..1f042c1b 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -20,7 +20,7 @@ import sentry_sdk
from django.contrib.messages import constants as messages
from sentry_sdk.integrations.django import DjangoIntegration
-from pydis_site.utils import get_git_sha
+from pydis_site.constants import GIT_SHA
if typing.TYPE_CHECKING:
from django.contrib.auth.models import User
@@ -35,7 +35,7 @@ sentry_sdk.init(
dsn=env('SITE_SENTRY_DSN'),
integrations=[DjangoIntegration()],
send_default_pii=True,
- release=f"pydis-site@{get_git_sha()}"
+ release=f"pydis-site@{GIT_SHA}"
)
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
diff --git a/pydis_site/tests/test_utils.py b/pydis_site/tests/test_utils.py
deleted file mode 100644
index f1419860..00000000
--- a/pydis_site/tests/test_utils.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.test import TestCase
-
-from pydis_site.utils import get_git_sha
-from pydis_site.utils.utils import GIT_SHA
-
-
-class UtilsTests(TestCase):
-
- def test_git_sha(self):
- """Test that the get_git_sha returns the correct SHA."""
- self.assertEqual(get_git_sha(), GIT_SHA)
diff --git a/pydis_site/utils/__init__.py b/pydis_site/utils/__init__.py
deleted file mode 100644
index bb91b3d8..00000000
--- a/pydis_site/utils/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .utils import get_git_sha
-
-__all__ = ['get_git_sha']
diff --git a/pydis_site/utils/utils.py b/pydis_site/utils/utils.py
deleted file mode 100644
index 2033ea19..00000000
--- a/pydis_site/utils/utils.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import git
-
-# Git SHA
-repo = git.Repo(search_parent_directories=True)
-GIT_SHA = repo.head.object.hexsha
-
-
-def get_git_sha() -> str:
- """Get the Git SHA for this repo."""
- return GIT_SHA
--
cgit v1.2.3
From 464408462cdb737b1eb78777c356253f9d5f0a4a Mon Sep 17 00:00:00 2001
From: rohanjnr
Date: Wed, 26 Aug 2020 20:51:05 +0530
Subject: add pagination for GET request on /bot/users endpoint
Pagination is done via PageNumberPagination, i.e, each page contains a specific number of `user` objects.
---
pydis_site/apps/api/viewsets/bot/user.py | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 9571b3d7..b016bb66 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,3 +1,4 @@
+from rest_framework.pagination import PageNumberPagination
from rest_framework.viewsets import ModelViewSet
from rest_framework_bulk import BulkCreateModelMixin
@@ -5,17 +6,28 @@ from pydis_site.apps.api.models.bot.user import User
from pydis_site.apps.api.serializers import UserSerializer
+class UserListPagination(PageNumberPagination):
+ """Custom pagination class for the User Model."""
+
+ page_size = 10000
+ page_size_query_param = "page_size"
+
+
class UserViewSet(BulkCreateModelMixin, ModelViewSet):
"""
View providing CRUD operations on Discord users through the bot.
## Routes
### GET /bot/users
- Returns all users currently known.
+ Returns all users currently known with pagination.
#### Response format
- >>> [
- ... {
+ >>> {
+ ... 'count': 95000,
+ ... 'next': "http://api.pythondiscord.com/bot/users?page=2",
+ ... 'previous': None,
+ ... 'results': [
+ ... {
... 'id': 409107086526644234,
... 'name': "Python",
... 'discriminator': 4329,
@@ -26,8 +38,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
... 458226699344019457
... ],
... 'in_guild': True
- ... }
- ... ]
+ ... },
+ ... ]
+ ... }
+
+ #### Query Parameters
+ - page_size: Number of Users in one page.
+ - page: Page number
#### Status codes
- 200: returned on success
@@ -118,4 +135,5 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
"""
serializer_class = UserSerializer
- queryset = User.objects
+ queryset = User.objects.all()
+ pagination_class = UserListPagination
--
cgit v1.2.3
From 8e636a54b449f44f5bff56577a05d9a6a2dd72c0 Mon Sep 17 00:00:00 2001
From: rohanjnr
Date: Wed, 26 Aug 2020 21:59:46 +0530
Subject: add support for bulk updates on user model
implemented a method to handle bulk updates on user model via a new endpoint: /bot/users/bulk_patch
---
pydis_site/apps/api/serializers.py | 77 +++++++++++++++++++++++++++++++-
pydis_site/apps/api/viewsets/bot/user.py | 60 +++++++++++++++++++++++++
2 files changed, 136 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 52e0d972..757faeae 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -1,5 +1,13 @@
"""Converters from Django models to data interchange formats and back."""
-from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError
+from django.db.models.query import QuerySet
+from rest_framework.serializers import (
+ ListSerializer,
+ ModelSerializer,
+ PrimaryKeyRelatedField,
+ ValidationError
+)
+from rest_framework.settings import api_settings
+from rest_framework.utils import html
from rest_framework.validators import UniqueTogetherValidator
from rest_framework_bulk import BulkSerializerMixin
@@ -260,6 +268,72 @@ class TagSerializer(ModelSerializer):
fields = ('title', 'embed')
+class UserListSerializer(ListSerializer):
+ """List serializer for User model to handle bulk updates."""
+
+ def to_internal_value(self, data: list) -> list:
+ """
+ Overriding `to_internal_value` function with a few changes to support bulk updates.
+
+ List of dicts of native values <- List of dicts of primitive datatypes.
+ """
+ if html.is_html_input(data):
+ data = html.parse_html_list(data, default=[])
+
+ if not isinstance(data, list):
+ message = self.error_messages['not_a_list'].format(
+ input_type=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ }, code='not_a_list')
+
+ if not self.allow_empty and len(data) == 0:
+ message = self.error_messages['empty']
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ }, code='empty')
+
+ ret = []
+ errors = []
+
+ for item in data:
+ # inserted code
+ # bug: https://github.com/miki725/django-rest-framework-bulk/issues/68
+ # -----------------
+ try:
+ self.child.instance = self.instance.get(id=item['id'])
+ except User.DoesNotExist:
+ self.child.instance = None
+ # -----------------
+ self.child.initial_data = item
+ try:
+ validated = self.child.run_validation(item)
+ except ValidationError as exc:
+ errors.append(exc.detail)
+ else:
+ ret.append(validated)
+ errors.append({})
+
+ if any(errors):
+ raise ValidationError(errors)
+
+ return ret
+
+ def update(self, instance: QuerySet, validated_data: list) -> list:
+ """Override update method to support bulk updates."""
+ instance_mapping = {user.id: user for user in instance}
+ data_mapping = {item['id']: item for item in validated_data}
+
+ updated = []
+ for book_id, data in data_mapping.items():
+ book = instance_mapping.get(book_id, None)
+ if book is not None:
+ updated.append(self.child.update(book, data))
+
+ return updated
+
+
class UserSerializer(BulkSerializerMixin, ModelSerializer):
"""A class providing (de-)serialization of `User` instances."""
@@ -269,6 +343,7 @@ class UserSerializer(BulkSerializerMixin, ModelSerializer):
model = User
fields = ('id', 'name', 'discriminator', 'roles', 'in_guild')
depth = 1
+ list_serializer_class = UserListSerializer
class NominationSerializer(ModelSerializer):
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index b016bb66..d64ca113 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,4 +1,8 @@
+from rest_framework import status
+from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
+from rest_framework.request import Request
+from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework_bulk import BulkCreateModelMixin
@@ -137,3 +141,59 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all()
pagination_class = UserListPagination
+
+ @action(detail=False, methods=["PATCH"])
+ def bulk_patch(self, request: Request) -> Response:
+ """
+ Update multiple User objects in a single request.
+
+ ## Route
+ ### PATCH /bot/users/bulk_patch
+ Update all users with the IDs.
+ `id` field is mandatory, rest are optional.
+
+ #### Request body
+ >>> [
+ ... {
+ ... 'id': int,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
+ ... },
+ ... {
+ ... 'id': int,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
+ ... },
+ ... ]
+
+ #### Status codes
+ - 200: Returned on success.
+ - 400: if the request body was invalid, see response body for details.
+ """
+ queryset = self.get_queryset()
+ try:
+ object_ids = [item["id"] for item in request.data]
+ except KeyError:
+ # user ID not provided in request body.
+ resp = {
+ "Error": "User ID not provided."
+ }
+ return Response(resp, status=status.HTTP_400_BAD_REQUEST)
+
+ filtered_instances = queryset.filter(id__in=object_ids)
+
+ serializer = self.get_serializer(
+ instance=filtered_instances,
+ data=request.data,
+ many=True,
+ partial=True
+ )
+
+ if serializer.is_valid():
+ serializer.save()
+ return Response(serializer.data, status=status.HTTP_200_OK)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
--
cgit v1.2.3
From 792af719e7934e639535e8803e7cd0118ccf43fb Mon Sep 17 00:00:00 2001
From: rohanjnr
Date: Wed, 26 Aug 2020 22:19:24 +0530
Subject: Add reference links in UserListSerializer methods
---
pydis_site/apps/api/serializers.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 757faeae..6146584b 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -275,6 +275,8 @@ class UserListSerializer(ListSerializer):
"""
Overriding `to_internal_value` function with a few changes to support bulk updates.
+ ref: https://github.com/miki725/django-rest-framework-bulk/issues/68
+
List of dicts of native values <- List of dicts of primitive datatypes.
"""
if html.is_html_input(data):
@@ -299,7 +301,6 @@ class UserListSerializer(ListSerializer):
for item in data:
# inserted code
- # bug: https://github.com/miki725/django-rest-framework-bulk/issues/68
# -----------------
try:
self.child.instance = self.instance.get(id=item['id'])
@@ -321,7 +322,11 @@ class UserListSerializer(ListSerializer):
return ret
def update(self, instance: QuerySet, validated_data: list) -> list:
- """Override update method to support bulk updates."""
+ """
+ Override update method to support bulk updates.
+
+ ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update
+ """
instance_mapping = {user.id: user for user in instance}
data_mapping = {item['id']: item for item in validated_data}
--
cgit v1.2.3
From 303cb191626d330663d64d3a3814e2c215493765 Mon Sep 17 00:00:00 2001
From: rohanjnr
Date: Wed, 26 Aug 2020 23:29:32 +0530
Subject: Except AttributeError when self.instance is None and while fetching
User object from database.
---
pydis_site/apps/api/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 6146584b..0589ce77 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -304,7 +304,7 @@ class UserListSerializer(ListSerializer):
# -----------------
try:
self.child.instance = self.instance.get(id=item['id'])
- except User.DoesNotExist:
+ except (User.DoesNotExist, AttributeError):
self.child.instance = None
# -----------------
self.child.initial_data = item
--
cgit v1.2.3
From d52a20962bbda8f94646ff12c38eb71e2a5241ab Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:38:25 +0300
Subject: (Tag Cleanup): Removed Tags viewset.
---
pydis_site/apps/api/viewsets/bot/__init__.py | 1 -
pydis_site/apps/api/viewsets/bot/tag.py | 105 ---------------------------
2 files changed, 106 deletions(-)
delete mode 100644 pydis_site/apps/api/viewsets/bot/tag.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py
index e64e3988..84b87eab 100644
--- a/pydis_site/apps/api/viewsets/bot/__init__.py
+++ b/pydis_site/apps/api/viewsets/bot/__init__.py
@@ -9,5 +9,4 @@ from .off_topic_channel_name import OffTopicChannelNameViewSet
from .offensive_message import OffensiveMessageViewSet
from .reminder import ReminderViewSet
from .role import RoleViewSet
-from .tag import TagViewSet
from .user import UserViewSet
diff --git a/pydis_site/apps/api/viewsets/bot/tag.py b/pydis_site/apps/api/viewsets/bot/tag.py
deleted file mode 100644
index 7e9ba117..00000000
--- a/pydis_site/apps/api/viewsets/bot/tag.py
+++ /dev/null
@@ -1,105 +0,0 @@
-from rest_framework.viewsets import ModelViewSet
-
-from pydis_site.apps.api.models.bot.tag import Tag
-from pydis_site.apps.api.serializers import TagSerializer
-
-
-class TagViewSet(ModelViewSet):
- """
- View providing CRUD operations on tags shown by our bot.
-
- ## Routes
- ### GET /bot/tags
- Returns all tags in the database.
-
- #### Response format
- >>> [
- ... {
- ... 'title': "resources",
- ... 'embed': {
- ... 'content': "Did you really think I'd put something useful here?"
- ... }
- ... }
- ... ]
-
- #### Status codes
- - 200: returned on success
-
- ### GET /bot/tags/
- Gets a single tag by its title.
-
- #### Response format
- >>> {
- ... 'title': "My awesome tag",
- ... 'embed': {
- ... 'content': "totally not filler words"
- ... }
- ... }
-
- #### Status codes
- - 200: returned on success
- - 404: if a tag with the given `title` could not be found
-
- ### POST /bot/tags
- Adds a single tag to the database.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 201: returned on success
- - 400: if one of the given fields is invalid
-
- ### PUT /bot/tags/
- Update the tag with the given `title`.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 200: returned on success
- - 400: if the request body was invalid, see response body for details
- - 404: if the tag with the given `title` could not be found
-
- ### PATCH /bot/tags/
- Update the tag with the given `title`.
-
- #### Request body
- >>> {
- ... 'title': str,
- ... 'embed': dict
- ... }
-
- The embed structure is the same as the embed structure that the Discord API
- expects. You can view the documentation for it here:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- #### Status codes
- - 200: returned on success
- - 400: if the request body was invalid, see response body for details
- - 404: if the tag with the given `title` could not be found
-
- ### DELETE /bot/tags/
- Deletes the tag with the given `title`.
-
- #### Status codes
- - 204: returned on success
- - 404: if a tag with the given `title` does not exist
- """
-
- serializer_class = TagSerializer
- queryset = Tag.objects.all()
--
cgit v1.2.3
From 86409cf41469bd2e5b8d69988a729b3b88bb0a26 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:39:37 +0300
Subject: (Tag Cleanup): Removed Tags from Administration
---
pydis_site/apps/api/admin.py | 2 --
1 file changed, 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 0333fefc..dd1291b8 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -14,7 +14,6 @@ from .models import (
OffTopicChannelName,
OffensiveMessage,
Role,
- Tag,
User
)
@@ -64,5 +63,4 @@ admin.site.register(Nomination)
admin.site.register(OffensiveMessage)
admin.site.register(OffTopicChannelName)
admin.site.register(Role)
-admin.site.register(Tag)
admin.site.register(User)
--
cgit v1.2.3
From 14453c9607ed83e040c9b1ed000a31e47568fd69 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:41:16 +0300
Subject: (Tag Cleanup): Removed Tags serializer.
---
pydis_site/apps/api/serializers.py | 11 -----------
1 file changed, 11 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 52e0d972..90bd6f91 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -16,7 +16,6 @@ from .models import (
OffensiveMessage,
Reminder,
Role,
- Tag,
User
)
@@ -250,16 +249,6 @@ class RoleSerializer(ModelSerializer):
fields = ('id', 'name', 'colour', 'permissions', 'position')
-class TagSerializer(ModelSerializer):
- """A class providing (de-)serialization of `Tag` instances."""
-
- class Meta:
- """Metadata defined for the Django REST Framework."""
-
- model = Tag
- fields = ('title', 'embed')
-
-
class UserSerializer(BulkSerializerMixin, ModelSerializer):
"""A class providing (de-)serialization of `User` instances."""
--
cgit v1.2.3
From 05b0c725b45dd87c755a8475d08442a0603a515e Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:41:58 +0300
Subject: (Tag Cleanup): Removed Tags URL
---
pydis_site/apps/api/urls.py | 5 -----
1 file changed, 5 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py
index a4fd5b2e..4dbf93db 100644
--- a/pydis_site/apps/api/urls.py
+++ b/pydis_site/apps/api/urls.py
@@ -14,7 +14,6 @@ from .viewsets import (
OffensiveMessageViewSet,
ReminderViewSet,
RoleViewSet,
- TagViewSet,
UserViewSet
)
@@ -61,10 +60,6 @@ bot_router.register(
'roles',
RoleViewSet
)
-bot_router.register(
- 'tags',
- TagViewSet
-)
bot_router.register(
'users',
UserViewSet
--
cgit v1.2.3
From bb55875dcdaf838c41b1e3b2c3dc4fdf368b0f09 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:43:33 +0300
Subject: (Tag Cleanup): Removed Tags Model test.
---
pydis_site/apps/api/tests/test_models.py | 5 -----
1 file changed, 5 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py
index e0e347bb..853e6621 100644
--- a/pydis_site/apps/api/tests/test_models.py
+++ b/pydis_site/apps/api/tests/test_models.py
@@ -14,7 +14,6 @@ from pydis_site.apps.api.models import (
OffensiveMessage,
Reminder,
Role,
- Tag,
User
)
from pydis_site.apps.api.models.mixins import ModelReprMixin
@@ -104,10 +103,6 @@ class StringDunderMethodTests(SimpleTestCase):
),
creation=dt.utcnow()
),
- Tag(
- title='bob',
- embed={'content': "the builder"}
- ),
User(
id=5,
name='bob',
--
cgit v1.2.3
From 790c50cbc2d45134fc0b668bc21ca7e01e4af2a2 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:46:19 +0300
Subject: (Tag Cleanup): Removed Tags Model import from models __init__.py
---
pydis_site/apps/api/models/bot/__init__.py | 1 -
1 file changed, 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py
index efd98184..1673b434 100644
--- a/pydis_site/apps/api/models/bot/__init__.py
+++ b/pydis_site/apps/api/models/bot/__init__.py
@@ -11,5 +11,4 @@ from .off_topic_channel_name import OffTopicChannelName
from .offensive_message import OffensiveMessage
from .reminder import Reminder
from .role import Role
-from .tag import Tag
from .user import User
--
cgit v1.2.3
From b16809620f444872f4d295ee55e4e8e9cdde12c8 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:49:49 +0300
Subject: (Tag Cleanup): Moved embed validators from Tag model to utils.py
---
pydis_site/apps/api/models/mixins.py | 173 +++++++++++++++++++++++++++++++++++
1 file changed, 173 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py
index 5d75b78b..2d658cb9 100644
--- a/pydis_site/apps/api/models/mixins.py
+++ b/pydis_site/apps/api/models/mixins.py
@@ -1,4 +1,177 @@
+from collections.abc import Mapping
from operator import itemgetter
+from typing import Any, Dict
+
+from django.core.exceptions import ValidationError
+from django.core.validators import MaxLengthValidator, MinLengthValidator
+
+
+def is_bool_validator(value: Any) -> None:
+ """Validates if a given value is of type bool."""
+ if not isinstance(value, bool):
+ raise ValidationError(f"This field must be of type bool, not {type(value)}.")
+
+
+def validate_tag_embed_fields(fields: dict) -> None:
+ """Raises a ValidationError if any of the given embed fields is invalid."""
+ field_validators = {
+ 'name': (MaxLengthValidator(limit_value=256),),
+ 'value': (MaxLengthValidator(limit_value=1024),),
+ 'inline': (is_bool_validator,),
+ }
+
+ required_fields = ('name', 'value')
+
+ for field in fields:
+ if not isinstance(field, Mapping):
+ raise ValidationError("Embed fields must be a mapping.")
+
+ if not all(required_field in field for required_field in required_fields):
+ raise ValidationError(
+ f"Embed fields must contain the following fields: {', '.join(required_fields)}."
+ )
+
+ for field_name, value in field.items():
+ if field_name not in field_validators:
+ raise ValidationError(f"Unknown embed field field: {field_name!r}.")
+
+ for validator in field_validators[field_name]:
+ validator(value)
+
+
+def validate_tag_embed_footer(footer: Dict[str, str]) -> None:
+ """Raises a ValidationError if the given footer is invalid."""
+ field_validators = {
+ 'text': (
+ MinLengthValidator(
+ limit_value=1,
+ message="Footer text must not be empty."
+ ),
+ MaxLengthValidator(limit_value=2048)
+ ),
+ 'icon_url': (),
+ 'proxy_icon_url': ()
+ }
+
+ if not isinstance(footer, Mapping):
+ raise ValidationError("Embed footer must be a mapping.")
+
+ for field_name, value in footer.items():
+ if field_name not in field_validators:
+ raise ValidationError(f"Unknown embed footer field: {field_name!r}.")
+
+ for validator in field_validators[field_name]:
+ validator(value)
+
+
+def validate_tag_embed_author(author: Any) -> None:
+ """Raises a ValidationError if the given author is invalid."""
+ field_validators = {
+ 'name': (
+ MinLengthValidator(
+ limit_value=1,
+ message="Embed author name must not be empty."
+ ),
+ MaxLengthValidator(limit_value=256)
+ ),
+ 'url': (),
+ 'icon_url': (),
+ 'proxy_icon_url': ()
+ }
+
+ if not isinstance(author, Mapping):
+ raise ValidationError("Embed author must be a mapping.")
+
+ for field_name, value in author.items():
+ if field_name not in field_validators:
+ raise ValidationError(f"Unknown embed author field: {field_name!r}.")
+
+ for validator in field_validators[field_name]:
+ validator(value)
+
+
+def validate_tag_embed(embed: Any) -> None:
+ """
+ Validate a JSON document containing an embed as possible to send on Discord.
+
+ This attempts to rebuild the validation used by Discord
+ as well as possible by checking for various embed limits so we can
+ ensure that any embed we store here will also be accepted as a
+ valid embed by the Discord API.
+
+ Using this directly is possible, although not intended - you usually
+ stick this onto the `validators` keyword argument of model fields.
+
+ Example:
+
+ >>> from django.contrib.postgres import fields as pgfields
+ >>> from django.db import models
+ >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed
+ >>> class MyMessage(models.Model):
+ ... embed = pgfields.JSONField(
+ ... validators=(
+ ... validate_tag_embed,
+ ... )
+ ... )
+ ... # ...
+ ...
+
+ Args:
+ embed (Any):
+ A dictionary describing the contents of this embed.
+ See the official documentation for a full reference
+ of accepted keys by this dictionary:
+ https://discordapp.com/developers/docs/resources/channel#embed-object
+
+ Raises:
+ ValidationError:
+ In case the given embed is deemed invalid, a `ValidationError`
+ is raised which in turn will allow Django to display errors
+ as appropriate.
+ """
+ all_keys = {
+ 'title', 'type', 'description', 'url', 'timestamp',
+ 'color', 'footer', 'image', 'thumbnail', 'video',
+ 'provider', 'author', 'fields'
+ }
+ one_required_of = {'description', 'fields', 'image', 'title', 'video'}
+ field_validators = {
+ 'title': (
+ MinLengthValidator(
+ limit_value=1,
+ message="Embed title must not be empty."
+ ),
+ MaxLengthValidator(limit_value=256)
+ ),
+ 'description': (MaxLengthValidator(limit_value=2048),),
+ 'fields': (
+ MaxLengthValidator(limit_value=25),
+ validate_tag_embed_fields
+ ),
+ 'footer': (validate_tag_embed_footer,),
+ 'author': (validate_tag_embed_author,)
+ }
+
+ if not embed:
+ raise ValidationError("Tag embed must not be empty.")
+
+ elif not isinstance(embed, Mapping):
+ raise ValidationError("Tag embed must be a mapping.")
+
+ elif not any(field in embed for field in one_required_of):
+ raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.")
+
+ for required_key in one_required_of:
+ if required_key in embed and not embed[required_key]:
+ raise ValidationError(f"Key {required_key!r} must not be empty.")
+
+ for field_name, value in embed.items():
+ if field_name not in all_keys:
+ raise ValidationError(f"Unknown field name: {field_name!r}")
+
+ if field_name in field_validators:
+ for validator in field_validators[field_name]:
+ validator(value)
from django.db import models
--
cgit v1.2.3
From 1f623b6629bdbc854323dc71d149a647355031c6 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:52:05 +0300
Subject: (Tag Cleanup): Removed `tag` from embed validator names due Tags will
be removed.
---
pydis_site/apps/api/models/mixins.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py
index 2d658cb9..e95888b7 100644
--- a/pydis_site/apps/api/models/mixins.py
+++ b/pydis_site/apps/api/models/mixins.py
@@ -12,7 +12,7 @@ def is_bool_validator(value: Any) -> None:
raise ValidationError(f"This field must be of type bool, not {type(value)}.")
-def validate_tag_embed_fields(fields: dict) -> None:
+def validate_embed_fields(fields: dict) -> None:
"""Raises a ValidationError if any of the given embed fields is invalid."""
field_validators = {
'name': (MaxLengthValidator(limit_value=256),),
@@ -39,7 +39,7 @@ def validate_tag_embed_fields(fields: dict) -> None:
validator(value)
-def validate_tag_embed_footer(footer: Dict[str, str]) -> None:
+def validate_embed_footer(footer: Dict[str, str]) -> None:
"""Raises a ValidationError if the given footer is invalid."""
field_validators = {
'text': (
@@ -64,7 +64,7 @@ def validate_tag_embed_footer(footer: Dict[str, str]) -> None:
validator(value)
-def validate_tag_embed_author(author: Any) -> None:
+def validate_embed_author(author: Any) -> None:
"""Raises a ValidationError if the given author is invalid."""
field_validators = {
'name': (
@@ -90,7 +90,7 @@ def validate_tag_embed_author(author: Any) -> None:
validator(value)
-def validate_tag_embed(embed: Any) -> None:
+def validate_embed(embed: Any) -> None:
"""
Validate a JSON document containing an embed as possible to send on Discord.
@@ -106,11 +106,11 @@ def validate_tag_embed(embed: Any) -> None:
>>> from django.contrib.postgres import fields as pgfields
>>> from django.db import models
- >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed
+ >>> from pydis_site.apps.api.models.utils import validate_embed
>>> class MyMessage(models.Model):
... embed = pgfields.JSONField(
... validators=(
- ... validate_tag_embed,
+ ... validate_embed,
... )
... )
... # ...
@@ -146,10 +146,10 @@ def validate_tag_embed(embed: Any) -> None:
'description': (MaxLengthValidator(limit_value=2048),),
'fields': (
MaxLengthValidator(limit_value=25),
- validate_tag_embed_fields
+ validate_embed_fields
),
- 'footer': (validate_tag_embed_footer,),
- 'author': (validate_tag_embed_author,)
+ 'footer': (validate_embed_footer,),
+ 'author': (validate_embed_author,)
}
if not embed:
--
cgit v1.2.3
From 9b6b9d9d01811bbe2d9a1964970e3b4a38180623 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:55:09 +0300
Subject: (Tag Cleanup): Replaced import from Tags model with utils.py
validator import.
---
pydis_site/apps/api/models/bot/message.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py
index 78dcbf1d..e929ea25 100644
--- a/pydis_site/apps/api/models/bot/message.py
+++ b/pydis_site/apps/api/models/bot/message.py
@@ -5,9 +5,8 @@ from django.core.validators import MinValueValidator
from django.db import models
from django.utils import timezone
-from pydis_site.apps.api.models.bot.tag import validate_tag_embed
from pydis_site.apps.api.models.bot.user import User
-from pydis_site.apps.api.models.mixins import ModelReprMixin
+from pydis_site.apps.api.models.utils import ModelReprMixin, validate_embed
class Message(ModelReprMixin, models.Model):
@@ -47,7 +46,7 @@ class Message(ModelReprMixin, models.Model):
)
embeds = pgfields.ArrayField(
pgfields.JSONField(
- validators=(validate_tag_embed,)
+ validators=(validate_embed,)
),
blank=True,
help_text="Embeds attached to this message."
--
cgit v1.2.3
From 1029c56eb7e1f986a540a58bc2e657db6925a93a Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 08:57:27 +0300
Subject: (Tag Cleanup): Replaced import from Tags model with utils.py
validator import in validators test.
---
pydis_site/apps/api/tests/test_validators.py | 56 ++++++++++++++--------------
1 file changed, 28 insertions(+), 28 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py
index 241af08c..8bb7b917 100644
--- a/pydis_site/apps/api/tests/test_validators.py
+++ b/pydis_site/apps/api/tests/test_validators.py
@@ -5,7 +5,7 @@ from django.test import TestCase
from ..models.bot.bot_setting import validate_bot_setting_name
from ..models.bot.offensive_message import future_date_validator
-from ..models.bot.tag import validate_tag_embed
+from ..models.utils import validate_embed
REQUIRED_KEYS = (
@@ -25,77 +25,77 @@ class BotSettingValidatorTests(TestCase):
class TagEmbedValidatorTests(TestCase):
def test_rejects_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed('non-empty non-mapping')
+ validate_embed('non-empty non-mapping')
def test_rejects_missing_required_keys(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'unknown': "key"
})
def test_rejects_one_correct_one_incorrect(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'provider': "??",
'title': ""
})
def test_rejects_empty_required_key(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': ''
})
def test_rejects_list_as_embed(self):
with self.assertRaises(ValidationError):
- validate_tag_embed([])
+ validate_embed([])
def test_rejects_required_keys_and_unknown_keys(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "the duck walked up to the lemonade stand",
'and': "he said to the man running the stand"
})
def test_rejects_too_long_title(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': 'a' * 257
})
def test_rejects_too_many_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [{} for _ in range(26)]
})
def test_rejects_too_long_description(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'description': 'd' * 2049
})
def test_allows_valid_embed(self):
- validate_tag_embed({
+ validate_embed({
'title': "My embed",
'description': "look at my embed, my embed is amazing"
})
def test_allows_unvalidated_fields(self):
- validate_tag_embed({
+ validate_embed({
'title': "My embed",
'provider': "what am I??"
})
def test_rejects_fields_as_list_of_non_mappings(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': ['abc']
})
def test_rejects_fields_with_unknown_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'what': "is this field"
@@ -105,7 +105,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_fields_with_too_long_name(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "a" * 257
@@ -115,7 +115,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_one_correct_one_incorrect_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -131,7 +131,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_missing_required_field_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -142,7 +142,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_invalid_inline_field_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "Totally valid",
@@ -153,7 +153,7 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_valid_fields(self):
- validate_tag_embed({
+ validate_embed({
'fields': [
{
'name': "valid",
@@ -174,14 +174,14 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_footer_as_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': []
})
def test_rejects_footer_with_unknown_fields(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'duck': "quack"
@@ -190,7 +190,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_footer_with_empty_text(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'text': ""
@@ -198,7 +198,7 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_footer_with_proper_values(self):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'footer': {
'text': "django good"
@@ -207,14 +207,14 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_as_non_mapping(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': []
})
def test_rejects_author_with_unknown_field(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'field': "that is unknown"
@@ -223,7 +223,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_with_empty_name(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'name': ""
@@ -232,7 +232,7 @@ class TagEmbedValidatorTests(TestCase):
def test_rejects_author_with_one_correct_one_incorrect(self):
with self.assertRaises(ValidationError):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
# Relies on "dictionary insertion order remembering" (D.I.O.R.) behaviour
@@ -242,7 +242,7 @@ class TagEmbedValidatorTests(TestCase):
})
def test_allows_author_with_proper_values(self):
- validate_tag_embed({
+ validate_embed({
'title': "whatever",
'author': {
'name': "Bob"
--
cgit v1.2.3
From 5df2d613bc72aad60fed73d2703dff55093def03 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 09:02:48 +0300
Subject: (Tag Cleanup): Removed tag import in models __init__.py
---
pydis_site/apps/api/models/__init__.py | 1 -
1 file changed, 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py
index 1d0ab7ea..e3f928e1 100644
--- a/pydis_site/apps/api/models/__init__.py
+++ b/pydis_site/apps/api/models/__init__.py
@@ -12,7 +12,6 @@ from .bot import (
OffTopicChannelName,
Reminder,
Role,
- Tag,
User
)
from .log_entry import LogEntry
--
cgit v1.2.3
From 494b735f762bf1780448db4591a4be4850dcdd4a Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 09:17:43 +0300
Subject: (Tag Cleanup): Removed Tag model
---
.../apps/api/migrations/0019_deletedmessage.py | 2 +-
pydis_site/apps/api/models/bot/tag.py | 25 ----------------------
2 files changed, 1 insertion(+), 26 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py
index 33746253..6b848d64 100644
--- a/pydis_site/apps/api/migrations/0019_deletedmessage.py
+++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py
@@ -18,7 +18,7 @@ class Migration(migrations.Migration):
('id', 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.')])),
('channel_id', 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.')])),
('content', models.CharField(help_text='The content of this message, taken from Discord.', max_length=2000)),
- ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)),
+ ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), help_text='Embeds attached to this message.', size=None)),
('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')),
('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')),
],
diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py
index 5e53582f..790ad37a 100644
--- a/pydis_site/apps/api/models/bot/tag.py
+++ b/pydis_site/apps/api/models/bot/tag.py
@@ -1,12 +1,8 @@
from collections.abc import Mapping
from typing import Any, Dict
-from django.contrib.postgres import fields as pgfields
from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, MinLengthValidator
-from django.db import models
-
-from pydis_site.apps.api.models.mixins import ModelReprMixin
def is_bool_validator(value: Any) -> None:
@@ -175,24 +171,3 @@ def validate_tag_embed(embed: Any) -> None:
if field_name in field_validators:
for validator in field_validators[field_name]:
validator(value)
-
-
-class Tag(ModelReprMixin, models.Model):
- """A tag providing (hopefully) useful information."""
-
- title = models.CharField(
- max_length=100,
- help_text=(
- "The title of this tag, shown in searches and providing "
- "a quick overview over what this embed contains."
- ),
- primary_key=True
- )
- embed = pgfields.JSONField(
- help_text="The actual embed shown by this tag.",
- validators=(validate_tag_embed,)
- )
-
- def __str__(self):
- """Returns the title of this tag, for display purposes."""
- return self.title
--
cgit v1.2.3
From ad5183a3956321c8956defa3f32e399400662bf6 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 09:20:48 +0300
Subject: (Tag Cleanup): Removed unnecessary tag validation migration.
---
pydis_site/apps/api/migrations/0008_tag_embed_validator.py | 7 -------
1 file changed, 7 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
index d53ddb90..d92042d2 100644
--- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
+++ b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py
@@ -1,7 +1,5 @@
# Generated by Django 2.1.1 on 2018-09-23 10:07
-import pydis_site.apps.api.models.bot.tag
-import django.contrib.postgres.fields.jsonb
from django.db import migrations
@@ -12,9 +10,4 @@ class Migration(migrations.Migration):
]
operations = [
- migrations.AlterField(
- model_name='tag',
- name='embed',
- field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]),
- ),
]
--
cgit v1.2.3
From a11d3a38a03b63a2018785917a56fe54c1b1f027 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 09:21:17 +0300
Subject: (Tag Cleanup): Removed Tag model file.
---
pydis_site/apps/api/models/bot/tag.py | 173 ----------------------------------
1 file changed, 173 deletions(-)
delete mode 100644 pydis_site/apps/api/models/bot/tag.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py
deleted file mode 100644
index 790ad37a..00000000
--- a/pydis_site/apps/api/models/bot/tag.py
+++ /dev/null
@@ -1,173 +0,0 @@
-from collections.abc import Mapping
-from typing import Any, Dict
-
-from django.core.exceptions import ValidationError
-from django.core.validators import MaxLengthValidator, MinLengthValidator
-
-
-def is_bool_validator(value: Any) -> None:
- """Validates if a given value is of type bool."""
- if not isinstance(value, bool):
- raise ValidationError(f"This field must be of type bool, not {type(value)}.")
-
-
-def validate_tag_embed_fields(fields: dict) -> None:
- """Raises a ValidationError if any of the given embed fields is invalid."""
- field_validators = {
- 'name': (MaxLengthValidator(limit_value=256),),
- 'value': (MaxLengthValidator(limit_value=1024),),
- 'inline': (is_bool_validator,),
- }
-
- required_fields = ('name', 'value')
-
- for field in fields:
- if not isinstance(field, Mapping):
- raise ValidationError("Embed fields must be a mapping.")
-
- if not all(required_field in field for required_field in required_fields):
- raise ValidationError(
- f"Embed fields must contain the following fields: {', '.join(required_fields)}."
- )
-
- for field_name, value in field.items():
- if field_name not in field_validators:
- raise ValidationError(f"Unknown embed field field: {field_name!r}.")
-
- for validator in field_validators[field_name]:
- validator(value)
-
-
-def validate_tag_embed_footer(footer: Dict[str, str]) -> None:
- """Raises a ValidationError if the given footer is invalid."""
- field_validators = {
- 'text': (
- MinLengthValidator(
- limit_value=1,
- message="Footer text must not be empty."
- ),
- MaxLengthValidator(limit_value=2048)
- ),
- 'icon_url': (),
- 'proxy_icon_url': ()
- }
-
- if not isinstance(footer, Mapping):
- raise ValidationError("Embed footer must be a mapping.")
-
- for field_name, value in footer.items():
- if field_name not in field_validators:
- raise ValidationError(f"Unknown embed footer field: {field_name!r}.")
-
- for validator in field_validators[field_name]:
- validator(value)
-
-
-def validate_tag_embed_author(author: Any) -> None:
- """Raises a ValidationError if the given author is invalid."""
- field_validators = {
- 'name': (
- MinLengthValidator(
- limit_value=1,
- message="Embed author name must not be empty."
- ),
- MaxLengthValidator(limit_value=256)
- ),
- 'url': (),
- 'icon_url': (),
- 'proxy_icon_url': ()
- }
-
- if not isinstance(author, Mapping):
- raise ValidationError("Embed author must be a mapping.")
-
- for field_name, value in author.items():
- if field_name not in field_validators:
- raise ValidationError(f"Unknown embed author field: {field_name!r}.")
-
- for validator in field_validators[field_name]:
- validator(value)
-
-
-def validate_tag_embed(embed: Any) -> None:
- """
- Validate a JSON document containing an embed as possible to send on Discord.
-
- This attempts to rebuild the validation used by Discord
- as well as possible by checking for various embed limits so we can
- ensure that any embed we store here will also be accepted as a
- valid embed by the Discord API.
-
- Using this directly is possible, although not intended - you usually
- stick this onto the `validators` keyword argument of model fields.
-
- Example:
-
- >>> from django.contrib.postgres import fields as pgfields
- >>> from django.db import models
- >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed
- >>> class MyMessage(models.Model):
- ... embed = pgfields.JSONField(
- ... validators=(
- ... validate_tag_embed,
- ... )
- ... )
- ... # ...
- ...
-
- Args:
- embed (Any):
- A dictionary describing the contents of this embed.
- See the official documentation for a full reference
- of accepted keys by this dictionary:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- Raises:
- ValidationError:
- In case the given embed is deemed invalid, a `ValidationError`
- is raised which in turn will allow Django to display errors
- as appropriate.
- """
- all_keys = {
- 'title', 'type', 'description', 'url', 'timestamp',
- 'color', 'footer', 'image', 'thumbnail', 'video',
- 'provider', 'author', 'fields'
- }
- one_required_of = {'description', 'fields', 'image', 'title', 'video'}
- field_validators = {
- 'title': (
- MinLengthValidator(
- limit_value=1,
- message="Embed title must not be empty."
- ),
- MaxLengthValidator(limit_value=256)
- ),
- 'description': (MaxLengthValidator(limit_value=2048),),
- 'fields': (
- MaxLengthValidator(limit_value=25),
- validate_tag_embed_fields
- ),
- 'footer': (validate_tag_embed_footer,),
- 'author': (validate_tag_embed_author,)
- }
-
- if not embed:
- raise ValidationError("Tag embed must not be empty.")
-
- elif not isinstance(embed, Mapping):
- raise ValidationError("Tag embed must be a mapping.")
-
- elif not any(field in embed for field in one_required_of):
- raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.")
-
- for required_key in one_required_of:
- if required_key in embed and not embed[required_key]:
- raise ValidationError(f"Key {required_key!r} must not be empty.")
-
- for field_name, value in embed.items():
- if field_name not in all_keys:
- raise ValidationError(f"Unknown field name: {field_name!r}")
-
- if field_name in field_validators:
- for validator in field_validators[field_name]:
- validator(value)
--
cgit v1.2.3
From 5357d5baf3234550c09f88fa38a9f436503f570c Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 09:21:48 +0300
Subject: (Tag Cleanup): Added Tag removal migration
---
pydis_site/apps/api/migrations/0051_delete_tag.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 pydis_site/apps/api/migrations/0051_delete_tag.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0051_delete_tag.py b/pydis_site/apps/api/migrations/0051_delete_tag.py
new file mode 100644
index 00000000..bada5788
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0051_delete_tag.py
@@ -0,0 +1,16 @@
+# Generated by Django 2.2.11 on 2020-04-01 06:15
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0050_remove_infractions_active_default_value'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='Tag',
+ ),
+ ]
--
cgit v1.2.3
From 99485facba10d1f0d918e8c0ab152372db89c7b9 Mon Sep 17 00:00:00 2001
From: ks123
Date: Wed, 1 Apr 2020 09:25:15 +0300
Subject: (Tag Cleanup): Removed Tag viewset from viewsets __init__.py
---
pydis_site/apps/api/viewsets/__init__.py | 1 -
1 file changed, 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py
index 8699517e..dfbb880d 100644
--- a/pydis_site/apps/api/viewsets/__init__.py
+++ b/pydis_site/apps/api/viewsets/__init__.py
@@ -10,7 +10,6 @@ from .bot import (
OffTopicChannelNameViewSet,
ReminderViewSet,
RoleViewSet,
- TagViewSet,
UserViewSet
)
from .log_entry import LogEntryViewSet
--
cgit v1.2.3
From 2b2f491e476c4564899c8716fde189da4a493287 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 27 Aug 2020 07:34:41 +0300
Subject: Move import to beginning of models mixins file
---
pydis_site/apps/api/models/mixins.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py
index e95888b7..692d14f7 100644
--- a/pydis_site/apps/api/models/mixins.py
+++ b/pydis_site/apps/api/models/mixins.py
@@ -4,6 +4,7 @@ from typing import Any, Dict
from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, MinLengthValidator
+from django.db import models
def is_bool_validator(value: Any) -> None:
@@ -173,8 +174,6 @@ def validate_embed(embed: Any) -> None:
for validator in field_validators[field_name]:
validator(value)
-from django.db import models
-
class ModelReprMixin:
"""Mixin providing a `__repr__()` to display model class name and initialisation parameters."""
--
cgit v1.2.3
From 2011d2f2c30dc67c1f34e82e5593f8e4ce1243dd Mon Sep 17 00:00:00 2001
From: Karlis S
Date: Thu, 27 Aug 2020 04:44:08 +0000
Subject: Move some parts from Mixins file to utils
---
pydis_site/apps/api/models/mixins.py | 172 ----------------------------------
pydis_site/apps/api/models/utils.py | 174 +++++++++++++++++++++++++++++++++++
2 files changed, 174 insertions(+), 172 deletions(-)
create mode 100644 pydis_site/apps/api/models/utils.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py
index 692d14f7..5d75b78b 100644
--- a/pydis_site/apps/api/models/mixins.py
+++ b/pydis_site/apps/api/models/mixins.py
@@ -1,180 +1,8 @@
-from collections.abc import Mapping
from operator import itemgetter
-from typing import Any, Dict
-from django.core.exceptions import ValidationError
-from django.core.validators import MaxLengthValidator, MinLengthValidator
from django.db import models
-def is_bool_validator(value: Any) -> None:
- """Validates if a given value is of type bool."""
- if not isinstance(value, bool):
- raise ValidationError(f"This field must be of type bool, not {type(value)}.")
-
-
-def validate_embed_fields(fields: dict) -> None:
- """Raises a ValidationError if any of the given embed fields is invalid."""
- field_validators = {
- 'name': (MaxLengthValidator(limit_value=256),),
- 'value': (MaxLengthValidator(limit_value=1024),),
- 'inline': (is_bool_validator,),
- }
-
- required_fields = ('name', 'value')
-
- for field in fields:
- if not isinstance(field, Mapping):
- raise ValidationError("Embed fields must be a mapping.")
-
- if not all(required_field in field for required_field in required_fields):
- raise ValidationError(
- f"Embed fields must contain the following fields: {', '.join(required_fields)}."
- )
-
- for field_name, value in field.items():
- if field_name not in field_validators:
- raise ValidationError(f"Unknown embed field field: {field_name!r}.")
-
- for validator in field_validators[field_name]:
- validator(value)
-
-
-def validate_embed_footer(footer: Dict[str, str]) -> None:
- """Raises a ValidationError if the given footer is invalid."""
- field_validators = {
- 'text': (
- MinLengthValidator(
- limit_value=1,
- message="Footer text must not be empty."
- ),
- MaxLengthValidator(limit_value=2048)
- ),
- 'icon_url': (),
- 'proxy_icon_url': ()
- }
-
- if not isinstance(footer, Mapping):
- raise ValidationError("Embed footer must be a mapping.")
-
- for field_name, value in footer.items():
- if field_name not in field_validators:
- raise ValidationError(f"Unknown embed footer field: {field_name!r}.")
-
- for validator in field_validators[field_name]:
- validator(value)
-
-
-def validate_embed_author(author: Any) -> None:
- """Raises a ValidationError if the given author is invalid."""
- field_validators = {
- 'name': (
- MinLengthValidator(
- limit_value=1,
- message="Embed author name must not be empty."
- ),
- MaxLengthValidator(limit_value=256)
- ),
- 'url': (),
- 'icon_url': (),
- 'proxy_icon_url': ()
- }
-
- if not isinstance(author, Mapping):
- raise ValidationError("Embed author must be a mapping.")
-
- for field_name, value in author.items():
- if field_name not in field_validators:
- raise ValidationError(f"Unknown embed author field: {field_name!r}.")
-
- for validator in field_validators[field_name]:
- validator(value)
-
-
-def validate_embed(embed: Any) -> None:
- """
- Validate a JSON document containing an embed as possible to send on Discord.
-
- This attempts to rebuild the validation used by Discord
- as well as possible by checking for various embed limits so we can
- ensure that any embed we store here will also be accepted as a
- valid embed by the Discord API.
-
- Using this directly is possible, although not intended - you usually
- stick this onto the `validators` keyword argument of model fields.
-
- Example:
-
- >>> from django.contrib.postgres import fields as pgfields
- >>> from django.db import models
- >>> from pydis_site.apps.api.models.utils import validate_embed
- >>> class MyMessage(models.Model):
- ... embed = pgfields.JSONField(
- ... validators=(
- ... validate_embed,
- ... )
- ... )
- ... # ...
- ...
-
- Args:
- embed (Any):
- A dictionary describing the contents of this embed.
- See the official documentation for a full reference
- of accepted keys by this dictionary:
- https://discordapp.com/developers/docs/resources/channel#embed-object
-
- Raises:
- ValidationError:
- In case the given embed is deemed invalid, a `ValidationError`
- is raised which in turn will allow Django to display errors
- as appropriate.
- """
- all_keys = {
- 'title', 'type', 'description', 'url', 'timestamp',
- 'color', 'footer', 'image', 'thumbnail', 'video',
- 'provider', 'author', 'fields'
- }
- one_required_of = {'description', 'fields', 'image', 'title', 'video'}
- field_validators = {
- 'title': (
- MinLengthValidator(
- limit_value=1,
- message="Embed title must not be empty."
- ),
- MaxLengthValidator(limit_value=256)
- ),
- 'description': (MaxLengthValidator(limit_value=2048),),
- 'fields': (
- MaxLengthValidator(limit_value=25),
- validate_embed_fields
- ),
- 'footer': (validate_embed_footer,),
- 'author': (validate_embed_author,)
- }
-
- if not embed:
- raise ValidationError("Tag embed must not be empty.")
-
- elif not isinstance(embed, Mapping):
- raise ValidationError("Tag embed must be a mapping.")
-
- elif not any(field in embed for field in one_required_of):
- raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.")
-
- for required_key in one_required_of:
- if required_key in embed and not embed[required_key]:
- raise ValidationError(f"Key {required_key!r} must not be empty.")
-
- for field_name, value in embed.items():
- if field_name not in all_keys:
- raise ValidationError(f"Unknown field name: {field_name!r}")
-
- if field_name in field_validators:
- for validator in field_validators[field_name]:
- validator(value)
-
-
class ModelReprMixin:
"""Mixin providing a `__repr__()` to display model class name and initialisation parameters."""
diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py
new file mode 100644
index 00000000..97a79507
--- /dev/null
+++ b/pydis_site/apps/api/models/utils.py
@@ -0,0 +1,174 @@
+from collections.abc import Mapping
+from operator import itemgetter
+from typing import Any, Dict
+
+from django.core.exceptions import ValidationError
+from django.core.validators import MaxLengthValidator, MinLengthValidator
+
+
+def is_bool_validator(value: Any) -> None:
+ """Validates if a given value is of type bool."""
+ if not isinstance(value, bool):
+ raise ValidationError(f"This field must be of type bool, not {type(value)}.")
+
+
+def validate_embed_fields(fields: dict) -> None:
+ """Raises a ValidationError if any of the given embed fields is invalid."""
+ field_validators = {
+ 'name': (MaxLengthValidator(limit_value=256),),
+ 'value': (MaxLengthValidator(limit_value=1024),),
+ 'inline': (is_bool_validator,),
+ }
+
+ required_fields = ('name', 'value')
+
+ for field in fields:
+ if not isinstance(field, Mapping):
+ raise ValidationError("Embed fields must be a mapping.")
+
+ if not all(required_field in field for required_field in required_fields):
+ raise ValidationError(
+ f"Embed fields must contain the following fields: {', '.join(required_fields)}."
+ )
+
+ for field_name, value in field.items():
+ if field_name not in field_validators:
+ raise ValidationError(f"Unknown embed field field: {field_name!r}.")
+
+ for validator in field_validators[field_name]:
+ validator(value)
+
+
+def validate_embed_footer(footer: Dict[str, str]) -> None:
+ """Raises a ValidationError if the given footer is invalid."""
+ field_validators = {
+ 'text': (
+ MinLengthValidator(
+ limit_value=1,
+ message="Footer text must not be empty."
+ ),
+ MaxLengthValidator(limit_value=2048)
+ ),
+ 'icon_url': (),
+ 'proxy_icon_url': ()
+ }
+
+ if not isinstance(footer, Mapping):
+ raise ValidationError("Embed footer must be a mapping.")
+
+ for field_name, value in footer.items():
+ if field_name not in field_validators:
+ raise ValidationError(f"Unknown embed footer field: {field_name!r}.")
+
+ for validator in field_validators[field_name]:
+ validator(value)
+
+
+def validate_embed_author(author: Any) -> None:
+ """Raises a ValidationError if the given author is invalid."""
+ field_validators = {
+ 'name': (
+ MinLengthValidator(
+ limit_value=1,
+ message="Embed author name must not be empty."
+ ),
+ MaxLengthValidator(limit_value=256)
+ ),
+ 'url': (),
+ 'icon_url': (),
+ 'proxy_icon_url': ()
+ }
+
+ if not isinstance(author, Mapping):
+ raise ValidationError("Embed author must be a mapping.")
+
+ for field_name, value in author.items():
+ if field_name not in field_validators:
+ raise ValidationError(f"Unknown embed author field: {field_name!r}.")
+
+ for validator in field_validators[field_name]:
+ validator(value)
+
+
+def validate_embed(embed: Any) -> None:
+ """
+ Validate a JSON document containing an embed as possible to send on Discord.
+
+ This attempts to rebuild the validation used by Discord
+ as well as possible by checking for various embed limits so we can
+ ensure that any embed we store here will also be accepted as a
+ valid embed by the Discord API.
+
+ Using this directly is possible, although not intended - you usually
+ stick this onto the `validators` keyword argument of model fields.
+
+ Example:
+
+ >>> from django.contrib.postgres import fields as pgfields
+ >>> from django.db import models
+ >>> from pydis_site.apps.api.models.utils import validate_embed
+ >>> class MyMessage(models.Model):
+ ... embed = pgfields.JSONField(
+ ... validators=(
+ ... validate_embed,
+ ... )
+ ... )
+ ... # ...
+ ...
+
+ Args:
+ embed (Any):
+ A dictionary describing the contents of this embed.
+ See the official documentation for a full reference
+ of accepted keys by this dictionary:
+ https://discordapp.com/developers/docs/resources/channel#embed-object
+
+ Raises:
+ ValidationError:
+ In case the given embed is deemed invalid, a `ValidationError`
+ is raised which in turn will allow Django to display errors
+ as appropriate.
+ """
+ all_keys = {
+ 'title', 'type', 'description', 'url', 'timestamp',
+ 'color', 'footer', 'image', 'thumbnail', 'video',
+ 'provider', 'author', 'fields'
+ }
+ one_required_of = {'description', 'fields', 'image', 'title', 'video'}
+ field_validators = {
+ 'title': (
+ MinLengthValidator(
+ limit_value=1,
+ message="Embed title must not be empty."
+ ),
+ MaxLengthValidator(limit_value=256)
+ ),
+ 'description': (MaxLengthValidator(limit_value=2048),),
+ 'fields': (
+ MaxLengthValidator(limit_value=25),
+ validate_embed_fields
+ ),
+ 'footer': (validate_embed_footer,),
+ 'author': (validate_embed_author,)
+ }
+
+ if not embed:
+ raise ValidationError("Tag embed must not be empty.")
+
+ elif not isinstance(embed, Mapping):
+ raise ValidationError("Tag embed must be a mapping.")
+
+ elif not any(field in embed for field in one_required_of):
+ raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.")
+
+ for required_key in one_required_of:
+ if required_key in embed and not embed[required_key]:
+ raise ValidationError(f"Key {required_key!r} must not be empty.")
+
+ for field_name, value in embed.items():
+ if field_name not in all_keys:
+ raise ValidationError(f"Unknown field name: {field_name!r}")
+
+ if field_name in field_validators:
+ for validator in field_validators[field_name]:
+ validator(value)
--
cgit v1.2.3
From 1ab4ccbbbcf38dddace5855ed9521219821b7698 Mon Sep 17 00:00:00 2001
From: Karlis S
Date: Thu, 27 Aug 2020 04:48:09 +0000
Subject: Remove unused import from models utils
---
pydis_site/apps/api/models/utils.py | 1 -
1 file changed, 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py
index 97a79507..107231ba 100644
--- a/pydis_site/apps/api/models/utils.py
+++ b/pydis_site/apps/api/models/utils.py
@@ -1,5 +1,4 @@
from collections.abc import Mapping
-from operator import itemgetter
from typing import Any, Dict
from django.core.exceptions import ValidationError
--
cgit v1.2.3
From fd2909c936efb9ab2285896b297b405c4dd7a1fb Mon Sep 17 00:00:00 2001
From: Karlis S
Date: Thu, 27 Aug 2020 05:35:01 +0000
Subject: Move last parts from mixins to utils and delete mixins
---
pydis_site/apps/api/models/mixins.py | 31 -------------------------------
pydis_site/apps/api/models/utils.py | 30 ++++++++++++++++++++++++++++++
2 files changed, 30 insertions(+), 31 deletions(-)
delete mode 100644 pydis_site/apps/api/models/mixins.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py
deleted file mode 100644
index 5d75b78b..00000000
--- a/pydis_site/apps/api/models/mixins.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from operator import itemgetter
-
-from django.db import models
-
-
-class ModelReprMixin:
- """Mixin providing a `__repr__()` to display model class name and initialisation parameters."""
-
- def __repr__(self):
- """Returns the current model class name and initialisation parameters."""
- attributes = ' '.join(
- f'{attribute}={value!r}'
- for attribute, value in sorted(
- self.__dict__.items(),
- key=itemgetter(0)
- )
- if not attribute.startswith('_')
- )
- return f'<{self.__class__.__name__}({attributes})>'
-
-
-class ModelTimestampMixin(models.Model):
- """Mixin providing created_at and updated_at fields."""
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- """Metaconfig for the mixin."""
-
- abstract = True
diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py
index 107231ba..692d14f7 100644
--- a/pydis_site/apps/api/models/utils.py
+++ b/pydis_site/apps/api/models/utils.py
@@ -1,8 +1,10 @@
from collections.abc import Mapping
+from operator import itemgetter
from typing import Any, Dict
from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, MinLengthValidator
+from django.db import models
def is_bool_validator(value: Any) -> None:
@@ -171,3 +173,31 @@ def validate_embed(embed: Any) -> None:
if field_name in field_validators:
for validator in field_validators[field_name]:
validator(value)
+
+
+class ModelReprMixin:
+ """Mixin providing a `__repr__()` to display model class name and initialisation parameters."""
+
+ def __repr__(self):
+ """Returns the current model class name and initialisation parameters."""
+ attributes = ' '.join(
+ f'{attribute}={value!r}'
+ for attribute, value in sorted(
+ self.__dict__.items(),
+ key=itemgetter(0)
+ )
+ if not attribute.startswith('_')
+ )
+ return f'<{self.__class__.__name__}({attributes})>'
+
+
+class ModelTimestampMixin(models.Model):
+ """Mixin providing created_at and updated_at fields."""
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ """Metaconfig for the mixin."""
+
+ abstract = True
--
cgit v1.2.3
From 4eeb841724e7603dffc5334a6e4aa7b7b7ced997 Mon Sep 17 00:00:00 2001
From: Karlis S
Date: Thu, 27 Aug 2020 05:37:55 +0000
Subject: Fix FilterList model mixins import path
---
pydis_site/apps/api/models/bot/filter_list.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py
index d279e137..cb4acb68 100644
--- a/pydis_site/apps/api/models/bot/filter_list.py
+++ b/pydis_site/apps/api/models/bot/filter_list.py
@@ -1,6 +1,6 @@
from django.db import models
-from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin
+from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin
class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model):
--
cgit v1.2.3
From feafa0950d9132350e36f58fbae57121363d3277 Mon Sep 17 00:00:00 2001
From: Karlis S
Date: Thu, 27 Aug 2020 05:47:47 +0000
Subject: Still move mixins back to its own file
---
pydis_site/apps/api/models/bot/filter_list.py | 2 +-
pydis_site/apps/api/models/mixins.py | 31 +++++++++++++++++++++++++++
pydis_site/apps/api/models/utils.py | 30 --------------------------
3 files changed, 32 insertions(+), 31 deletions(-)
create mode 100644 pydis_site/apps/api/models/mixins.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py
index cb4acb68..d279e137 100644
--- a/pydis_site/apps/api/models/bot/filter_list.py
+++ b/pydis_site/apps/api/models/bot/filter_list.py
@@ -1,6 +1,6 @@
from django.db import models
-from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin
+from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin
class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model):
diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py
new file mode 100644
index 00000000..5d75b78b
--- /dev/null
+++ b/pydis_site/apps/api/models/mixins.py
@@ -0,0 +1,31 @@
+from operator import itemgetter
+
+from django.db import models
+
+
+class ModelReprMixin:
+ """Mixin providing a `__repr__()` to display model class name and initialisation parameters."""
+
+ def __repr__(self):
+ """Returns the current model class name and initialisation parameters."""
+ attributes = ' '.join(
+ f'{attribute}={value!r}'
+ for attribute, value in sorted(
+ self.__dict__.items(),
+ key=itemgetter(0)
+ )
+ if not attribute.startswith('_')
+ )
+ return f'<{self.__class__.__name__}({attributes})>'
+
+
+class ModelTimestampMixin(models.Model):
+ """Mixin providing created_at and updated_at fields."""
+
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ """Metaconfig for the mixin."""
+
+ abstract = True
diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py
index 692d14f7..107231ba 100644
--- a/pydis_site/apps/api/models/utils.py
+++ b/pydis_site/apps/api/models/utils.py
@@ -1,10 +1,8 @@
from collections.abc import Mapping
-from operator import itemgetter
from typing import Any, Dict
from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, MinLengthValidator
-from django.db import models
def is_bool_validator(value: Any) -> None:
@@ -173,31 +171,3 @@ def validate_embed(embed: Any) -> None:
if field_name in field_validators:
for validator in field_validators[field_name]:
validator(value)
-
-
-class ModelReprMixin:
- """Mixin providing a `__repr__()` to display model class name and initialisation parameters."""
-
- def __repr__(self):
- """Returns the current model class name and initialisation parameters."""
- attributes = ' '.join(
- f'{attribute}={value!r}'
- for attribute, value in sorted(
- self.__dict__.items(),
- key=itemgetter(0)
- )
- if not attribute.startswith('_')
- )
- return f'<{self.__class__.__name__}({attributes})>'
-
-
-class ModelTimestampMixin(models.Model):
- """Mixin providing created_at and updated_at fields."""
-
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
-
- class Meta:
- """Metaconfig for the mixin."""
-
- abstract = True
--
cgit v1.2.3
From e1f75feb74c24b262b276534070e4ffb3bf295b3 Mon Sep 17 00:00:00 2001
From: Karlis S
Date: Thu, 27 Aug 2020 05:53:09 +0000
Subject: Fix import paths of mixins in message model
---
pydis_site/apps/api/models/bot/message.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py
index e929ea25..f6ae55a5 100644
--- a/pydis_site/apps/api/models/bot/message.py
+++ b/pydis_site/apps/api/models/bot/message.py
@@ -6,7 +6,8 @@ from django.db import models
from django.utils import timezone
from pydis_site.apps.api.models.bot.user import User
-from pydis_site.apps.api.models.utils import ModelReprMixin, validate_embed
+from pydis_site.apps.api.models.mixins import ModelReprMixin
+from pydis_site.apps.api.models.utils import validate_embed
class Message(ModelReprMixin, models.Model):
--
cgit v1.2.3
From ec689ad42d28de55d47f9d3730389ae7e179d565 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 27 Aug 2020 08:57:28 +0300
Subject: Fix embed validator location in migration
---
pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
index e617e1c9..b03c8a18 100644
--- a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
+++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
@@ -16,6 +16,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='deletedmessage',
name='embeds',
- field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), blank=True, help_text='Embeds attached to this message.', size=None),
+ field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), blank=True, help_text='Embeds attached to this message.', size=None),
),
]
--
cgit v1.2.3
From b43477faa59970adffc93c58743a4abce53b1547 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 27 Aug 2020 09:01:28 +0300
Subject: Replace bad import on migration
---
pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
index b03c8a18..124c6a57 100644
--- a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
+++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py
@@ -3,7 +3,7 @@
import django.contrib.postgres.fields
import django.contrib.postgres.fields.jsonb
from django.db import migrations
-import pydis_site.apps.api.models.bot.tag
+import pydis_site.apps.api.models.utils
class Migration(migrations.Migration):
--
cgit v1.2.3
From 567f7f0c4a71ace555c9b3123ef50d6ae47756cd Mon Sep 17 00:00:00 2001
From: rohanjnr
Date: Fri, 28 Aug 2020 12:58:55 +0530
Subject: Add code to replace restframework_bulk package for bulk create and
simplify UserListSerializer
`to_internal_value()` function is not longer overriden in UserListSerializer, this is due to explicitly stating the `id` field in UserSerializer as mentioned in the documentation.
Override `create()` method in UserListSerializer and override `get_serializer()` method in `UserViewSet` to support bulk creation.
---
pydis_site/apps/api/serializers.py | 73 ++++++++------------------------
pydis_site/apps/api/viewsets/bot/user.py | 20 +++++++--
2 files changed, 35 insertions(+), 58 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 0589ce77..21c488a8 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -1,15 +1,13 @@
"""Converters from Django models to data interchange formats and back."""
from django.db.models.query import QuerySet
from rest_framework.serializers import (
+ IntegerField,
ListSerializer,
ModelSerializer,
PrimaryKeyRelatedField,
ValidationError
)
-from rest_framework.settings import api_settings
-from rest_framework.utils import html
from rest_framework.validators import UniqueTogetherValidator
-from rest_framework_bulk import BulkSerializerMixin
from .models import (
BotSetting,
@@ -271,55 +269,18 @@ class TagSerializer(ModelSerializer):
class UserListSerializer(ListSerializer):
"""List serializer for User model to handle bulk updates."""
- def to_internal_value(self, data: list) -> list:
- """
- Overriding `to_internal_value` function with a few changes to support bulk updates.
-
- ref: https://github.com/miki725/django-rest-framework-bulk/issues/68
+ def create(self, validated_data: list) -> list:
+ """Override create method to optimize django queries."""
+ present_users = User.objects.all()
+ new_users = []
+ present_user_ids = [user.id for user in present_users]
- List of dicts of native values <- List of dicts of primitive datatypes.
- """
- if html.is_html_input(data):
- data = html.parse_html_list(data, default=[])
+ for user_dict in validated_data:
+ if user_dict["id"] in present_user_ids:
+ raise ValidationError({"id": "User already exists."})
+ new_users.append(User(**user_dict))
- if not isinstance(data, list):
- message = self.error_messages['not_a_list'].format(
- input_type=type(data).__name__
- )
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: [message]
- }, code='not_a_list')
-
- if not self.allow_empty and len(data) == 0:
- message = self.error_messages['empty']
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: [message]
- }, code='empty')
-
- ret = []
- errors = []
-
- for item in data:
- # inserted code
- # -----------------
- try:
- self.child.instance = self.instance.get(id=item['id'])
- except (User.DoesNotExist, AttributeError):
- self.child.instance = None
- # -----------------
- self.child.initial_data = item
- try:
- validated = self.child.run_validation(item)
- except ValidationError as exc:
- errors.append(exc.detail)
- else:
- ret.append(validated)
- errors.append({})
-
- if any(errors):
- raise ValidationError(errors)
-
- return ret
+ return User.objects.bulk_create(new_users)
def update(self, instance: QuerySet, validated_data: list) -> list:
"""
@@ -331,17 +292,19 @@ class UserListSerializer(ListSerializer):
data_mapping = {item['id']: item for item in validated_data}
updated = []
- for book_id, data in data_mapping.items():
- book = instance_mapping.get(book_id, None)
- if book is not None:
- updated.append(self.child.update(book, data))
+ for user_id, data in data_mapping.items():
+ user = instance_mapping.get(user_id, None)
+ if user:
+ updated.append(self.child.update(user, data))
return updated
-class UserSerializer(BulkSerializerMixin, ModelSerializer):
+class UserSerializer(ModelSerializer):
"""A class providing (de-)serialization of `User` instances."""
+ id = IntegerField(min_value=0)
+
class Meta:
"""Metadata defined for the Django REST Framework."""
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index d64ca113..d015fe71 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -3,8 +3,8 @@ from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
from rest_framework.request import Request
from rest_framework.response import Response
+from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
-from rest_framework_bulk import BulkCreateModelMixin
from pydis_site.apps.api.models.bot.user import User
from pydis_site.apps.api.serializers import UserSerializer
@@ -17,7 +17,7 @@ class UserListPagination(PageNumberPagination):
page_size_query_param = "page_size"
-class UserViewSet(BulkCreateModelMixin, ModelViewSet):
+class UserViewSet(ModelViewSet):
"""
View providing CRUD operations on Discord users through the bot.
@@ -142,7 +142,14 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
queryset = User.objects.all()
pagination_class = UserListPagination
- @action(detail=False, methods=["PATCH"])
+ def get_serializer(self, *args, **kwargs) -> ModelSerializer:
+ """Set Serializer many attribute to True if request body contains a list."""
+ if isinstance(kwargs.get('data', {}), list):
+ kwargs['many'] = True
+
+ return super().get_serializer(*args, **kwargs)
+
+ @action(detail=False, methods=["PATCH"], name='user-bulk-patch')
def bulk_patch(self, request: Request) -> Response:
"""
Update multiple User objects in a single request.
@@ -186,6 +193,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
filtered_instances = queryset.filter(id__in=object_ids)
+ if filtered_instances.count() != len(object_ids):
+ # If all user objects passed in request.body are not present in the database.
+ resp = {
+ "Error": "User object not found."
+ }
+ return Response(resp, status=status.HTTP_404_NOT_FOUND)
+
serializer = self.get_serializer(
instance=filtered_instances,
data=request.data,
--
cgit v1.2.3
From 2aa12e7f605f56ed31f14aa1c93bf4ef48323678 Mon Sep 17 00:00:00 2001
From: rohanjnr
Date: Fri, 28 Aug 2020 13:02:49 +0530
Subject: Add tests for bulk patch for User Model and additional test for bulk
creation of User Models.
---
pydis_site/apps/api/tests/test_users.py | 116 ++++++++++++++++++++++++++++++++
1 file changed, 116 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 4c0f6e27..a54e1189 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -45,6 +45,13 @@ class CreationTests(APISubdomainTestCase):
position=1
)
+ cls.user = User.objects.create(
+ id=11,
+ name="Name doesn't matter.",
+ discriminator=1122,
+ in_guild=True
+ )
+
def test_accepts_valid_data(self):
url = reverse('bot:user-list', host='api')
data = {
@@ -115,6 +122,115 @@ class CreationTests(APISubdomainTestCase):
response = self.client.post(url, data=data)
self.assertEqual(response.status_code, 400)
+ def test_returns_400_for_user_recreation(self):
+ """Return 400 if User is already present in database."""
+ url = reverse('bot:user-list', host='api')
+ data = [{
+ 'id': 11,
+ 'name': 'You saw nothing.',
+ 'discriminator': 112,
+ 'in_guild': True
+ }]
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+
+
+class MultiPatchTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.role_developer = Role.objects.create(
+ id=159,
+ name="Developer",
+ colour=2,
+ permissions=0b01010010101,
+ position=10,
+ )
+ cls.user_1 = User.objects.create(
+ id=1,
+ name="Patch test user 1.",
+ discriminator=1111,
+ in_guild=True
+ )
+ cls.user_2 = User.objects.create(
+ id=2,
+ name="Patch test user 2.",
+ discriminator=2222,
+ in_guild=True
+ )
+
+ def test_multiple_users_patch(self):
+ url = reverse("bot:user-bulk-patch", host="api")
+ data = [
+ {
+ "id": 1,
+ "name": "User 1 patched!",
+ "discriminator": 1010,
+ "roles": [self.role_developer.id],
+ "in_guild": False
+ },
+ {
+ "id": 2,
+ "name": "User 2 patched!"
+ }
+ ]
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json()[0], data[0])
+
+ user_2 = User.objects.get(id=2)
+ self.assertEqual(user_2.name, data[1]["name"])
+
+ def test_returns_400_for_missing_user_id(self):
+ url = reverse("bot:user-bulk-patch", host="api")
+ data = [
+ {
+ "name": "I am ghost user!",
+ "discriminator": 1010,
+ "roles": [self.role_developer.id],
+ "in_guild": False
+ },
+ {
+ "name": "patch me? whats my id?"
+ }
+ ]
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+
+ def test_returns_404_for_not_found_user(self):
+ url = reverse("bot:user-bulk-patch", host="api")
+ data = [
+ {
+ "id": 1,
+ "name": "User 1 patched again!!!",
+ "discriminator": 1010,
+ "roles": [self.role_developer.id],
+ "in_guild": False
+ },
+ {
+ "id": 22503405,
+ "name": "User unknown not patched!"
+ }
+ ]
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 404)
+
+ def test_returns_400_for_bad_data(self):
+ url = reverse("bot:user-bulk-patch", host="api")
+ data = [
+ {
+ "id": 1,
+ "in_guild": "Catch me!"
+ },
+ {
+ "id": 2,
+ "discriminator": "find me!"
+ }
+ ]
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+
class UserModelTests(APISubdomainTestCase):
@classmethod
--
cgit v1.2.3
From 982ef7515f9a9f66098c8047be1792fae9f05016 Mon Sep 17 00:00:00 2001
From: rohanjnr
Date: Fri, 28 Aug 2020 21:40:05 +0530
Subject: remove redundant if clause in update() method in UserListSeriazlier.
---
pydis_site/apps/api/serializers.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 21c488a8..0915658a 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -293,9 +293,8 @@ class UserListSerializer(ListSerializer):
updated = []
for user_id, data in data_mapping.items():
- user = instance_mapping.get(user_id, None)
- if user:
- updated.append(self.child.update(user, data))
+ user = instance_mapping.get(user_id)
+ updated.append(self.child.update(user, data))
return updated
--
cgit v1.2.3
From c1ec2f1f9fe443cdde3750b1df1c3d969df3e67e Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sun, 30 Aug 2020 08:20:25 +0300
Subject: Revert "Revert pull request #348"
This reverts commit dae68c46831ef14e6a0715add1025f9af1b5a73d.
---
.../api/migrations/0051_create_news_setting.py | 25 ----
.../api/migrations/0052_create_news_setting.py | 25 ++++
.../api/migrations/0052_remove_user_avatar_hash.py | 17 ---
.../migrations/0053_offtopicchannelname_used.py | 18 +++
.../api/migrations/0053_user_roles_to_array.py | 24 ----
.../api/migrations/0054_remove_user_avatar_hash.py | 17 +++
.../0054_user_invalidate_unknown_role.py | 21 ---
.../api/migrations/0055_merge_20200714_2027.py | 14 --
.../apps/api/migrations/0055_reminder_mentions.py | 20 ---
.../api/migrations/0055_user_roles_to_array.py | 24 ++++
.../api/migrations/0056_allow_blank_user_roles.py | 21 ---
.../0056_user_invalidate_unknown_role.py | 21 +++
.../api/migrations/0057_merge_20200716_0751.py | 14 --
.../apps/api/migrations/0057_reminder_mentions.py | 20 +++
.../api/migrations/0058_allow_blank_user_roles.py | 21 +++
.../migrations/0058_create_new_filterlist_model.py | 33 -----
.../migrations/0059_create_new_filterlist_model.py | 33 +++++
.../api/migrations/0059_populate_filterlists.py | 153 ---------------------
.../api/migrations/0060_populate_filterlists.py | 153 +++++++++++++++++++++
.../migrations/0060_populate_filterlists_fix.py | 85 ------------
.../migrations/0061_populate_filterlists_fix.py | 85 ++++++++++++
.../apps/api/models/bot/off_topic_channel_name.py | 5 +
.../apps/api/tests/test_off_topic_channel_names.py | 27 +++-
.../api/viewsets/bot/off_topic_channel_name.py | 27 +++-
24 files changed, 472 insertions(+), 431 deletions(-)
delete mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py
create mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py
delete mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py
create mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py
delete mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py
create mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py
delete mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py
delete mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py
delete mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py
create mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py
delete mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py
create mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py
delete mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py
create mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py
create mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py
delete mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py
create mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py
delete mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py
create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py
delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py
create mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py
deleted file mode 100644
index f18fdfb1..00000000
--- a/pydis_site/apps/api/migrations/0051_create_news_setting.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from django.db import migrations
-
-
-def up(apps, schema_editor):
- BotSetting = apps.get_model('api', 'BotSetting')
- setting = BotSetting(
- name='news',
- data={}
- ).save()
-
-
-def down(apps, schema_editor):
- BotSetting = apps.get_model('api', 'BotSetting')
- BotSetting.objects.get(name='news').delete()
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0050_remove_infractions_active_default_value'),
- ]
-
- operations = [
- migrations.RunPython(up, down)
- ]
diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py
new file mode 100644
index 00000000..b101d19d
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0052_create_news_setting.py
@@ -0,0 +1,25 @@
+from django.db import migrations
+
+
+def up(apps, schema_editor):
+ BotSetting = apps.get_model('api', 'BotSetting')
+ setting = BotSetting(
+ name='news',
+ data={}
+ ).save()
+
+
+def down(apps, schema_editor):
+ BotSetting = apps.get_model('api', 'BotSetting')
+ BotSetting.objects.get(name='news').delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0051_allow_blank_message_embeds'),
+ ]
+
+ operations = [
+ migrations.RunPython(up, down)
+ ]
diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py
deleted file mode 100644
index 26b3b954..00000000
--- a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 2.2.11 on 2020-05-27 07:17
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0051_create_news_setting'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='user',
- name='avatar_hash',
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py
new file mode 100644
index 00000000..b51ce1d2
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.11 on 2020-03-30 10:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0052_create_news_setting'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='offtopicchannelname',
+ name='used',
+ field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py
deleted file mode 100644
index 7ff3a548..00000000
--- a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 2.2.11 on 2020-06-02 13:42
-
-import django.contrib.postgres.fields
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0052_remove_user_avatar_hash'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='user',
- name='roles',
- ),
- migrations.AddField(
- model_name='user',
- name='roles',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py
new file mode 100644
index 00000000..be9fd948
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.2.11 on 2020-05-27 07:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0053_offtopicchannelname_used'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='user',
+ name='avatar_hash',
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py
deleted file mode 100644
index 96230015..00000000
--- a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 2.2.11 on 2020-06-02 20:08
-
-import django.contrib.postgres.fields
-import django.core.validators
-from django.db import migrations, models
-import pydis_site.apps.api.models.bot.user
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0053_user_roles_to_array'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='user',
- name='roles',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py
deleted file mode 100644
index f2a0e638..00000000
--- a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Generated by Django 3.0.8 on 2020-07-14 20:27
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0051_allow_blank_message_embeds'),
- ('api', '0054_user_invalidate_unknown_role'),
- ]
-
- operations = [
- ]
diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py
deleted file mode 100644
index d73b450d..00000000
--- a/pydis_site/apps/api/migrations/0055_reminder_mentions.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 2.2.14 on 2020-07-15 07:37
-
-import django.contrib.postgres.fields
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0054_user_invalidate_unknown_role'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='reminder',
- name='mentions',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py
new file mode 100644
index 00000000..e7b4a983
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.2.11 on 2020-06-02 13:42
+
+import django.contrib.postgres.fields
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0054_remove_user_avatar_hash'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='user',
+ name='roles',
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='roles',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py
deleted file mode 100644
index 489941c7..00000000
--- a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 3.0.8 on 2020-07-14 20:35
-
-import django.contrib.postgres.fields
-import django.core.validators
-from django.db import migrations, models
-import pydis_site.apps.api.models.bot.user
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0055_merge_20200714_2027'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='user',
- name='roles',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py
new file mode 100644
index 00000000..ab2696aa
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py
@@ -0,0 +1,21 @@
+# Generated by Django 2.2.11 on 2020-06-02 20:08
+
+import django.contrib.postgres.fields
+import django.core.validators
+from django.db import migrations, models
+import pydis_site.apps.api.models.bot.user
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0055_user_roles_to_array'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='roles',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py
deleted file mode 100644
index 47a6d2d4..00000000
--- a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Generated by Django 2.2.14 on 2020-07-16 07:51
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0055_reminder_mentions'),
- ('api', '0056_allow_blank_user_roles'),
- ]
-
- operations = [
- ]
diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py
new file mode 100644
index 00000000..fb829a17
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0057_reminder_mentions.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2.14 on 2020-07-15 07:37
+
+import django.contrib.postgres.fields
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0056_user_invalidate_unknown_role'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='reminder',
+ name='mentions',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py
new file mode 100644
index 00000000..8f7fddfc
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.0.8 on 2020-07-14 20:35
+
+import django.contrib.postgres.fields
+import django.core.validators
+from django.db import migrations, models
+import pydis_site.apps.api.models.bot.user
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0057_reminder_mentions'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='roles',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py
deleted file mode 100644
index aecfdad7..00000000
--- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Generated by Django 3.0.8 on 2020-07-15 11:23
-
-from django.db import migrations, models
-import pydis_site.apps.api.models.mixins
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ('api', '0057_merge_20200716_0751'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='FilterList',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('type', models.CharField(
- choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'),
- ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')],
- help_text='The type of allowlist this is on.', max_length=50)),
- ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')),
- ('content', models.TextField(help_text='The data to add to the allow or denylist.')),
- ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)),
- ],
- bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
- ),
- migrations.AddConstraint(
- model_name='filterlist',
- constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list')
- )
- ]
diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py
new file mode 100644
index 00000000..eac5542d
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.0.8 on 2020-07-15 11:23
+
+from django.db import migrations, models
+import pydis_site.apps.api.models.mixins
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('api', '0058_allow_blank_user_roles'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='FilterList',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('type', models.CharField(
+ choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'),
+ ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')],
+ help_text='The type of allowlist this is on.', max_length=50)),
+ ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')),
+ ('content', models.TextField(help_text='The data to add to the allow or denylist.')),
+ ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)),
+ ],
+ bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
+ ),
+ migrations.AddConstraint(
+ model_name='filterlist',
+ constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list')
+ )
+ ]
diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py
deleted file mode 100644
index 8c550191..00000000
--- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from django.db import migrations
-
-guild_invite_whitelist = [
- ("discord.gg/python", "Python Discord", True),
- ("discord.gg/4JJdJKb", "RLBot", True),
- ("discord.gg/djPtTRJ", "Kivy", True),
- ("discord.gg/QXyegWe", "Pyglet", True),
- ("discord.gg/9XsucTT", "Panda3D", True),
- ("discord.gg/AP3rq2k", "PyWeek", True),
- ("discord.gg/vSPsP9t", "Microsoft Python", True),
- ("discord.gg/bRCvFy9", "Discord.js Official", True),
- ("discord.gg/9zT7NHP", "Programming Discussions", True),
- ("discord.gg/ysd6M4r", "JetBrains Community", True),
- ("discord.gg/4xJeCgy", "Raspberry Pie", True),
- ("discord.gg/AStb3kZ", "Ren'Py", True),
- ("discord.gg/t655QNV", "Python Discord: Emojis 1", True),
- ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True),
- ("discord.gg/jTtgWuy", "Django", True),
- ("discord.gg/W9BypZF", "STEM", True),
- ("discord.gg/dpy", "discord.py", True),
- ("discord.gg/programming", "Programmers Hangout", True),
- ("discord.gg/qhGUjGD", "SpeakJS", True),
- ("discord.gg/eTbWSZj", "Functional Programming", True),
- ("discord.gg/r8yreB6", "PyGame", True),
- ("discord.gg/5UBnR3P", "Python Atlanta", True),
- ("discord.gg/ccyrDKv", "C#", True),
-]
-
-domain_name_blacklist = [
- ("pornhub.com", None, False),
- ("liveleak.com", None, False),
- ("grabify.link", None, False),
- ("bmwforum.co", None, False),
- ("leancoding.co", None, False),
- ("spottyfly.com", None, False),
- ("stopify.co", None, False),
- ("yoütu.be", None, False),
- ("discörd.com", None, False),
- ("minecräft.com", None, False),
- ("freegiftcards.co", None, False),
- ("disçordapp.com", None, False),
- ("fortnight.space", None, False),
- ("fortnitechat.site", None, False),
- ("joinmy.site", None, False),
- ("curiouscat.club", None, False),
- ("catsnthings.fun", None, False),
- ("yourtube.site", None, False),
- ("youtubeshort.watch", None, False),
- ("catsnthing.com", None, False),
- ("youtubeshort.pro", None, False),
- ("canadianlumberjacks.online", None, False),
- ("poweredbydialup.club", None, False),
- ("poweredbydialup.online", None, False),
- ("poweredbysecurity.org", None, False),
- ("poweredbysecurity.online", None, False),
- ("ssteam.site", None, False),
- ("steamwalletgift.com", None, False),
- ("discord.gift", None, False),
- ("lmgtfy.com", None, False),
-]
-
-filter_token_blacklist = [
- ("\bgoo+ks*\b", None, False),
- ("\bky+s+\b", None, False),
- ("\bki+ke+s*\b", None, False),
- ("\bbeaner+s?\b", None, False),
- ("\bcoo+ns*\b", None, False),
- ("\bnig+lets*\b", None, False),
- ("\bslant-eyes*\b", None, False),
- ("\btowe?l-?head+s*\b", None, False),
- ("\bchi*n+k+s*\b", None, False),
- ("\bspick*s*\b", None, False),
- ("\bkill* +(?:yo)?urself+\b", None, False),
- ("\bjew+s*\b", None, False),
- ("\bsuicide\b", None, False),
- ("\brape\b", None, False),
- ("\b(re+)tar+(d+|t+)(ed)?\b", None, False),
- ("\bta+r+d+\b", None, False),
- ("\bcunts*\b", None, False),
- ("\btrann*y\b", None, False),
- ("\bshemale\b", None, False),
- ("fa+g+s*", None, False),
- ("卐", None, False),
- ("卍", None, False),
- ("࿖", None, False),
- ("࿕", None, False),
- ("࿘", None, False),
- ("࿗", None, False),
- ("cuck(?!oo+)", None, False),
- ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False),
- ("fag+o+t+s*", None, False),
-]
-
-file_format_whitelist = [
- (".3gp", None, True),
- (".3g2", None, True),
- (".avi", None, True),
- (".bmp", None, True),
- (".gif", None, True),
- (".h264", None, True),
- (".jpg", None, True),
- (".jpeg", None, True),
- (".m4v", None, True),
- (".mkv", None, True),
- (".mov", None, True),
- (".mp4", None, True),
- (".mpeg", None, True),
- (".mpg", None, True),
- (".png", None, True),
- (".tiff", None, True),
- (".wmv", None, True),
- (".svg", None, True),
- (".psd", "Photoshop", True),
- (".ai", "Illustrator", True),
- (".aep", "After Effects", True),
- (".xcf", "GIMP", True),
- (".mp3", None, True),
- (".wav", None, True),
- (".ogg", None, True),
- (".webm", None, True),
- (".webp", None, True),
-]
-
-populate_data = {
- "FILTER_TOKEN": filter_token_blacklist,
- "DOMAIN_NAME": domain_name_blacklist,
- "FILE_FORMAT": file_format_whitelist,
- "GUILD_INVITE": guild_invite_whitelist,
-}
-
-
-class Migration(migrations.Migration):
- dependencies = [("api", "0058_create_new_filterlist_model")]
-
- def populate_filterlists(app, _):
- FilterList = app.get_model("api", "FilterList")
-
- for filterlist_type, metadata in populate_data.items():
- for content, comment, allowed in metadata:
- FilterList.objects.create(
- type=filterlist_type,
- allowed=allowed,
- content=content,
- comment=comment,
- )
-
- def clear_filterlists(app, _):
- FilterList = app.get_model("api", "FilterList")
- FilterList.objects.all().delete()
-
- operations = [
- migrations.RunPython(populate_filterlists, clear_filterlists)
- ]
diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py
new file mode 100644
index 00000000..35fde95a
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0060_populate_filterlists.py
@@ -0,0 +1,153 @@
+from django.db import migrations
+
+guild_invite_whitelist = [
+ ("discord.gg/python", "Python Discord", True),
+ ("discord.gg/4JJdJKb", "RLBot", True),
+ ("discord.gg/djPtTRJ", "Kivy", True),
+ ("discord.gg/QXyegWe", "Pyglet", True),
+ ("discord.gg/9XsucTT", "Panda3D", True),
+ ("discord.gg/AP3rq2k", "PyWeek", True),
+ ("discord.gg/vSPsP9t", "Microsoft Python", True),
+ ("discord.gg/bRCvFy9", "Discord.js Official", True),
+ ("discord.gg/9zT7NHP", "Programming Discussions", True),
+ ("discord.gg/ysd6M4r", "JetBrains Community", True),
+ ("discord.gg/4xJeCgy", "Raspberry Pie", True),
+ ("discord.gg/AStb3kZ", "Ren'Py", True),
+ ("discord.gg/t655QNV", "Python Discord: Emojis 1", True),
+ ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True),
+ ("discord.gg/jTtgWuy", "Django", True),
+ ("discord.gg/W9BypZF", "STEM", True),
+ ("discord.gg/dpy", "discord.py", True),
+ ("discord.gg/programming", "Programmers Hangout", True),
+ ("discord.gg/qhGUjGD", "SpeakJS", True),
+ ("discord.gg/eTbWSZj", "Functional Programming", True),
+ ("discord.gg/r8yreB6", "PyGame", True),
+ ("discord.gg/5UBnR3P", "Python Atlanta", True),
+ ("discord.gg/ccyrDKv", "C#", True),
+]
+
+domain_name_blacklist = [
+ ("pornhub.com", None, False),
+ ("liveleak.com", None, False),
+ ("grabify.link", None, False),
+ ("bmwforum.co", None, False),
+ ("leancoding.co", None, False),
+ ("spottyfly.com", None, False),
+ ("stopify.co", None, False),
+ ("yoütu.be", None, False),
+ ("discörd.com", None, False),
+ ("minecräft.com", None, False),
+ ("freegiftcards.co", None, False),
+ ("disçordapp.com", None, False),
+ ("fortnight.space", None, False),
+ ("fortnitechat.site", None, False),
+ ("joinmy.site", None, False),
+ ("curiouscat.club", None, False),
+ ("catsnthings.fun", None, False),
+ ("yourtube.site", None, False),
+ ("youtubeshort.watch", None, False),
+ ("catsnthing.com", None, False),
+ ("youtubeshort.pro", None, False),
+ ("canadianlumberjacks.online", None, False),
+ ("poweredbydialup.club", None, False),
+ ("poweredbydialup.online", None, False),
+ ("poweredbysecurity.org", None, False),
+ ("poweredbysecurity.online", None, False),
+ ("ssteam.site", None, False),
+ ("steamwalletgift.com", None, False),
+ ("discord.gift", None, False),
+ ("lmgtfy.com", None, False),
+]
+
+filter_token_blacklist = [
+ ("\bgoo+ks*\b", None, False),
+ ("\bky+s+\b", None, False),
+ ("\bki+ke+s*\b", None, False),
+ ("\bbeaner+s?\b", None, False),
+ ("\bcoo+ns*\b", None, False),
+ ("\bnig+lets*\b", None, False),
+ ("\bslant-eyes*\b", None, False),
+ ("\btowe?l-?head+s*\b", None, False),
+ ("\bchi*n+k+s*\b", None, False),
+ ("\bspick*s*\b", None, False),
+ ("\bkill* +(?:yo)?urself+\b", None, False),
+ ("\bjew+s*\b", None, False),
+ ("\bsuicide\b", None, False),
+ ("\brape\b", None, False),
+ ("\b(re+)tar+(d+|t+)(ed)?\b", None, False),
+ ("\bta+r+d+\b", None, False),
+ ("\bcunts*\b", None, False),
+ ("\btrann*y\b", None, False),
+ ("\bshemale\b", None, False),
+ ("fa+g+s*", None, False),
+ ("卐", None, False),
+ ("卍", None, False),
+ ("࿖", None, False),
+ ("࿕", None, False),
+ ("࿘", None, False),
+ ("࿗", None, False),
+ ("cuck(?!oo+)", None, False),
+ ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False),
+ ("fag+o+t+s*", None, False),
+]
+
+file_format_whitelist = [
+ (".3gp", None, True),
+ (".3g2", None, True),
+ (".avi", None, True),
+ (".bmp", None, True),
+ (".gif", None, True),
+ (".h264", None, True),
+ (".jpg", None, True),
+ (".jpeg", None, True),
+ (".m4v", None, True),
+ (".mkv", None, True),
+ (".mov", None, True),
+ (".mp4", None, True),
+ (".mpeg", None, True),
+ (".mpg", None, True),
+ (".png", None, True),
+ (".tiff", None, True),
+ (".wmv", None, True),
+ (".svg", None, True),
+ (".psd", "Photoshop", True),
+ (".ai", "Illustrator", True),
+ (".aep", "After Effects", True),
+ (".xcf", "GIMP", True),
+ (".mp3", None, True),
+ (".wav", None, True),
+ (".ogg", None, True),
+ (".webm", None, True),
+ (".webp", None, True),
+]
+
+populate_data = {
+ "FILTER_TOKEN": filter_token_blacklist,
+ "DOMAIN_NAME": domain_name_blacklist,
+ "FILE_FORMAT": file_format_whitelist,
+ "GUILD_INVITE": guild_invite_whitelist,
+}
+
+
+class Migration(migrations.Migration):
+ dependencies = [("api", "0059_create_new_filterlist_model")]
+
+ def populate_filterlists(app, _):
+ FilterList = app.get_model("api", "FilterList")
+
+ for filterlist_type, metadata in populate_data.items():
+ for content, comment, allowed in metadata:
+ FilterList.objects.create(
+ type=filterlist_type,
+ allowed=allowed,
+ content=content,
+ comment=comment,
+ )
+
+ def clear_filterlists(app, _):
+ FilterList = app.get_model("api", "FilterList")
+ FilterList.objects.all().delete()
+
+ operations = [
+ migrations.RunPython(populate_filterlists, clear_filterlists)
+ ]
diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py
deleted file mode 100644
index 53846f02..00000000
--- a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py
+++ /dev/null
@@ -1,85 +0,0 @@
-from django.db import migrations
-
-bad_guild_invite_whitelist = [
- ("discord.gg/python", "Python Discord", True),
- ("discord.gg/4JJdJKb", "RLBot", True),
- ("discord.gg/djPtTRJ", "Kivy", True),
- ("discord.gg/QXyegWe", "Pyglet", True),
- ("discord.gg/9XsucTT", "Panda3D", True),
- ("discord.gg/AP3rq2k", "PyWeek", True),
- ("discord.gg/vSPsP9t", "Microsoft Python", True),
- ("discord.gg/bRCvFy9", "Discord.js Official", True),
- ("discord.gg/9zT7NHP", "Programming Discussions", True),
- ("discord.gg/ysd6M4r", "JetBrains Community", True),
- ("discord.gg/4xJeCgy", "Raspberry Pie", True),
- ("discord.gg/AStb3kZ", "Ren'Py", True),
- ("discord.gg/t655QNV", "Python Discord: Emojis 1", True),
- ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True),
- ("discord.gg/jTtgWuy", "Django", True),
- ("discord.gg/W9BypZF", "STEM", True),
- ("discord.gg/dpy", "discord.py", True),
- ("discord.gg/programming", "Programmers Hangout", True),
- ("discord.gg/qhGUjGD", "SpeakJS", True),
- ("discord.gg/eTbWSZj", "Functional Programming", True),
- ("discord.gg/r8yreB6", "PyGame", True),
- ("discord.gg/5UBnR3P", "Python Atlanta", True),
- ("discord.gg/ccyrDKv", "C#", True),
-]
-
-guild_invite_whitelist = [
- ("267624335836053506", "Python Discord", True),
- ("348658686962696195", "RLBot", True),
- ("423249981340778496", "Kivy", True),
- ("438622377094414346", "Pyglet", True),
- ("524691714909274162", "Panda3D", True),
- ("666560367173828639", "PyWeek", True),
- ("702724176489873509", "Microsoft Python", True),
- ("222078108977594368", "Discord.js Official", True),
- ("238666723824238602", "Programming Discussions", True),
- ("433980600391696384", "JetBrains Community", True),
- ("204621105720328193", "Raspberry Pie", True),
- ("286633898581164032", "Ren'Py", True),
- ("440186186024222721", "Python Discord: Emojis 1", True),
- ("578587418123304970", "Python Discord: Emojis 2", True),
- ("159039020565790721", "Django", True),
- ("273944235143593984", "STEM", True),
- ("336642139381301249", "discord.py", True),
- ("244230771232079873", "Programmers Hangout", True),
- ("239433591950540801", "SpeakJS", True),
- ("280033776820813825", "Functional Programming", True),
- ("349505959032389632", "PyGame", True),
- ("488751051629920277", "Python Atlanta", True),
- ("143867839282020352", "C#", True),
-]
-
-
-class Migration(migrations.Migration):
- dependencies = [("api", "0059_populate_filterlists")]
-
- def fix_filterlist(app, _):
- FilterList = app.get_model("api", "FilterList")
- FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059.
-
- for content, comment, allowed in guild_invite_whitelist:
- FilterList.objects.create(
- type="GUILD_INVITE",
- allowed=allowed,
- content=content,
- comment=comment,
- )
-
- def restore_bad_filterlist(app, _):
- FilterList = app.get_model("api", "FilterList")
- FilterList.objects.filter(type="GUILD_INVITE").delete()
-
- for content, comment, allowed in bad_guild_invite_whitelist:
- FilterList.objects.create(
- type="GUILD_INVITE",
- allowed=allowed,
- content=content,
- comment=comment,
- )
-
- operations = [
- migrations.RunPython(fix_filterlist, restore_bad_filterlist)
- ]
diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py
new file mode 100644
index 00000000..eaaafb38
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py
@@ -0,0 +1,85 @@
+from django.db import migrations
+
+bad_guild_invite_whitelist = [
+ ("discord.gg/python", "Python Discord", True),
+ ("discord.gg/4JJdJKb", "RLBot", True),
+ ("discord.gg/djPtTRJ", "Kivy", True),
+ ("discord.gg/QXyegWe", "Pyglet", True),
+ ("discord.gg/9XsucTT", "Panda3D", True),
+ ("discord.gg/AP3rq2k", "PyWeek", True),
+ ("discord.gg/vSPsP9t", "Microsoft Python", True),
+ ("discord.gg/bRCvFy9", "Discord.js Official", True),
+ ("discord.gg/9zT7NHP", "Programming Discussions", True),
+ ("discord.gg/ysd6M4r", "JetBrains Community", True),
+ ("discord.gg/4xJeCgy", "Raspberry Pie", True),
+ ("discord.gg/AStb3kZ", "Ren'Py", True),
+ ("discord.gg/t655QNV", "Python Discord: Emojis 1", True),
+ ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True),
+ ("discord.gg/jTtgWuy", "Django", True),
+ ("discord.gg/W9BypZF", "STEM", True),
+ ("discord.gg/dpy", "discord.py", True),
+ ("discord.gg/programming", "Programmers Hangout", True),
+ ("discord.gg/qhGUjGD", "SpeakJS", True),
+ ("discord.gg/eTbWSZj", "Functional Programming", True),
+ ("discord.gg/r8yreB6", "PyGame", True),
+ ("discord.gg/5UBnR3P", "Python Atlanta", True),
+ ("discord.gg/ccyrDKv", "C#", True),
+]
+
+guild_invite_whitelist = [
+ ("267624335836053506", "Python Discord", True),
+ ("348658686962696195", "RLBot", True),
+ ("423249981340778496", "Kivy", True),
+ ("438622377094414346", "Pyglet", True),
+ ("524691714909274162", "Panda3D", True),
+ ("666560367173828639", "PyWeek", True),
+ ("702724176489873509", "Microsoft Python", True),
+ ("222078108977594368", "Discord.js Official", True),
+ ("238666723824238602", "Programming Discussions", True),
+ ("433980600391696384", "JetBrains Community", True),
+ ("204621105720328193", "Raspberry Pie", True),
+ ("286633898581164032", "Ren'Py", True),
+ ("440186186024222721", "Python Discord: Emojis 1", True),
+ ("578587418123304970", "Python Discord: Emojis 2", True),
+ ("159039020565790721", "Django", True),
+ ("273944235143593984", "STEM", True),
+ ("336642139381301249", "discord.py", True),
+ ("244230771232079873", "Programmers Hangout", True),
+ ("239433591950540801", "SpeakJS", True),
+ ("280033776820813825", "Functional Programming", True),
+ ("349505959032389632", "PyGame", True),
+ ("488751051629920277", "Python Atlanta", True),
+ ("143867839282020352", "C#", True),
+]
+
+
+class Migration(migrations.Migration):
+ dependencies = [("api", "0060_populate_filterlists")]
+
+ def fix_filterlist(app, _):
+ FilterList = app.get_model("api", "FilterList")
+ FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059.
+
+ for content, comment, allowed in guild_invite_whitelist:
+ FilterList.objects.create(
+ type="GUILD_INVITE",
+ allowed=allowed,
+ content=content,
+ comment=comment,
+ )
+
+ def restore_bad_filterlist(app, _):
+ FilterList = app.get_model("api", "FilterList")
+ FilterList.objects.filter(type="GUILD_INVITE").delete()
+
+ for content, comment, allowed in bad_guild_invite_whitelist:
+ FilterList.objects.create(
+ type="GUILD_INVITE",
+ allowed=allowed,
+ content=content,
+ comment=comment,
+ )
+
+ operations = [
+ migrations.RunPython(fix_filterlist, restore_bad_filterlist)
+ ]
diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py
index 20e77b9f..403c7465 100644
--- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py
+++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py
@@ -16,6 +16,11 @@ class OffTopicChannelName(ModelReprMixin, models.Model):
help_text="The actual channel name that will be used on our Discord server."
)
+ used = models.BooleanField(
+ default=False,
+ help_text="Whether or not this name has already been used during this rotation",
+ )
+
def __str__(self):
"""Returns the current off-topic name, for display purposes."""
return self.name
diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py
index bd42cd81..3ab8b22d 100644
--- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py
+++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py
@@ -10,12 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase):
self.client.force_authenticate(user=None)
def test_cannot_read_off_topic_channel_name_list(self):
+ """Return a 401 response when not authenticated."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
self.assertEqual(response.status_code, 401)
def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self):
+ """Return a 401 response when `random_items` provided and not authenticated."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(f'{url}?random_items=no')
@@ -24,6 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase):
class EmptyDatabaseTests(APISubdomainTestCase):
def test_returns_empty_object(self):
+ """Return empty list when no names in database."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -31,6 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
self.assertEqual(response.json(), [])
def test_returns_empty_list_with_get_all_param(self):
+ """Return empty list when no names and `random_items` param provided."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(f'{url}?random_items=5')
@@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
self.assertEqual(response.json(), [])
def test_returns_400_for_bad_random_items_param(self):
+ """Return error message when passing not integer as `random_items`."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(f'{url}?random_items=totally-a-valid-integer')
@@ -47,6 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
})
def test_returns_400_for_negative_random_items_param(self):
+ """Return error message when passing negative int as `random_items`."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(f'{url}?random_items=-5')
@@ -59,10 +65,11 @@ class EmptyDatabaseTests(APISubdomainTestCase):
class ListTests(APISubdomainTestCase):
@classmethod
def setUpTestData(cls):
- cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand')
- cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')
+ cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False)
+ cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True)
def test_returns_name_in_list(self):
+ """Return all off-topic channel names."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -76,11 +83,21 @@ class ListTests(APISubdomainTestCase):
)
def test_returns_single_item_with_random_items_param_set_to_1(self):
+ """Return not-used name instead used."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(f'{url}?random_items=1')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
+ self.assertEqual(response.json(), [self.test_name.name])
+
+ def test_running_out_of_names_with_random_parameter(self):
+ """Reset names `used` parameter to `False` when running out of names."""
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=2')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name])
class CreationTests(APISubdomainTestCase):
@@ -93,6 +110,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
def test_returns_201_for_unicode_chars(self):
+ """Accept all valid characters."""
url = reverse('bot:offtopicchannelname-list', host='api')
names = (
'𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹',
@@ -104,6 +122,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
def test_returns_400_for_missing_name_param(self):
+ """Return error message when name not provided."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.post(url)
self.assertEqual(response.status_code, 400)
@@ -112,6 +131,7 @@ class CreationTests(APISubdomainTestCase):
})
def test_returns_400_for_bad_name_param(self):
+ """Return error message when invalid characters provided."""
url = reverse('bot:offtopicchannelname-list', host='api')
invalid_names = (
'space between words',
@@ -134,18 +154,21 @@ class DeletionTests(APISubdomainTestCase):
cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')
def test_deleting_unknown_name_returns_404(self):
+ """Return 404 reponse when trying to delete unknown name."""
url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api')
response = self.client.delete(url)
self.assertEqual(response.status_code, 404)
def test_deleting_known_name_returns_204(self):
+ """Return 204 response when deleting was successful."""
url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api')
response = self.client.delete(url)
self.assertEqual(response.status_code, 204)
def test_name_gets_deleted(self):
+ """Name gets actually deleted."""
url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api')
response = self.client.delete(url)
diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
index d6da2399..826ad25e 100644
--- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
+++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
@@ -1,3 +1,4 @@
+from django.db.models import Case, Value, When
from django.db.models.query import QuerySet
from django.http.request import HttpRequest
from django.shortcuts import get_object_or_404
@@ -20,7 +21,9 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
Return all known off-topic channel names from the database.
If the `random_items` query parameter is given, for example using...
$ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5
- ... then the API will return `5` random items from the database.
+ ... then the API will return `5` random items from the database
+ that is not used in current rotation.
+ When running out of names, API will mark all names to not used and start new rotation.
#### Response format
Return a list of off-topic-channel names:
@@ -106,7 +109,27 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
'random_items': ["Must be a positive integer."]
})
- queryset = self.get_queryset().order_by('?')[:random_count]
+ queryset = self.get_queryset().order_by('used', '?')[:random_count]
+
+ # When any name is used in our listing then this means we reached end of round
+ # and we need to reset all other names `used` to False
+ if any(offtopic_name.used for offtopic_name in queryset):
+ # These names that we just got have to be excluded from updating used to False
+ self.get_queryset().update(
+ used=Case(
+ When(
+ name__in=(offtopic_name.name for offtopic_name in queryset),
+ then=Value(True)
+ ),
+ default=Value(False)
+ )
+ )
+ else:
+ # Otherwise mark selected names `used` to True
+ self.get_queryset().filter(
+ name__in=(offtopic_name.name for offtopic_name in queryset)
+ ).update(used=True)
+
serialized = self.serializer_class(queryset, many=True)
return Response(serialized.data)
--
cgit v1.2.3
From 0ec3148228d7c13a35626059cffdad9dbb0670c2 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sun, 30 Aug 2020 08:21:21 +0300
Subject: Revert "Fix mess with migrations"
This reverts commit 32342aca3309b4fd482841cb84e4f166152d5c33.
---
.../api/migrations/0051_create_news_setting.py | 25 ++++
.../api/migrations/0052_create_news_setting.py | 25 ----
.../migrations/0052_offtopicchannelname_used.py | 18 +++
.../api/migrations/0052_remove_user_avatar_hash.py | 17 +++
.../migrations/0053_offtopicchannelname_used.py | 18 ---
.../api/migrations/0053_user_roles_to_array.py | 24 ++++
.../api/migrations/0054_remove_user_avatar_hash.py | 17 ---
.../0054_user_invalidate_unknown_role.py | 21 +++
.../api/migrations/0055_merge_20200714_2027.py | 14 ++
.../apps/api/migrations/0055_reminder_mentions.py | 20 +++
.../api/migrations/0055_user_roles_to_array.py | 24 ----
.../api/migrations/0056_allow_blank_user_roles.py | 21 +++
.../0056_user_invalidate_unknown_role.py | 21 ---
.../api/migrations/0057_merge_20200716_0751.py | 14 ++
.../apps/api/migrations/0057_reminder_mentions.py | 20 ---
.../api/migrations/0058_allow_blank_user_roles.py | 21 ---
.../migrations/0058_create_new_filterlist_model.py | 33 +++++
.../migrations/0059_create_new_filterlist_model.py | 33 -----
.../api/migrations/0059_populate_filterlists.py | 153 +++++++++++++++++++++
.../api/migrations/0060_populate_filterlists.py | 153 ---------------------
.../migrations/0060_populate_filterlists_fix.py | 85 ++++++++++++
.../migrations/0061_populate_filterlists_fix.py | 85 ------------
22 files changed, 445 insertions(+), 417 deletions(-)
create mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py
delete mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py
create mode 100644 pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py
create mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py
delete mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py
create mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py
delete mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py
create mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py
create mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py
create mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py
delete mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py
create mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py
delete mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py
create mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py
delete mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py
delete mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py
create mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py
delete mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py
create mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py
delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py
create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py
delete mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py
new file mode 100644
index 00000000..f18fdfb1
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py
@@ -0,0 +1,25 @@
+from django.db import migrations
+
+
+def up(apps, schema_editor):
+ BotSetting = apps.get_model('api', 'BotSetting')
+ setting = BotSetting(
+ name='news',
+ data={}
+ ).save()
+
+
+def down(apps, schema_editor):
+ BotSetting = apps.get_model('api', 'BotSetting')
+ BotSetting.objects.get(name='news').delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0050_remove_infractions_active_default_value'),
+ ]
+
+ operations = [
+ migrations.RunPython(up, down)
+ ]
diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py
deleted file mode 100644
index b101d19d..00000000
--- a/pydis_site/apps/api/migrations/0052_create_news_setting.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from django.db import migrations
-
-
-def up(apps, schema_editor):
- BotSetting = apps.get_model('api', 'BotSetting')
- setting = BotSetting(
- name='news',
- data={}
- ).save()
-
-
-def down(apps, schema_editor):
- BotSetting = apps.get_model('api', 'BotSetting')
- BotSetting.objects.get(name='news').delete()
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0051_allow_blank_message_embeds'),
- ]
-
- operations = [
- migrations.RunPython(up, down)
- ]
diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py
new file mode 100644
index 00000000..dfdf3835
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.11 on 2020-03-30 10:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0051_create_news_setting'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='offtopicchannelname',
+ name='used',
+ field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py
new file mode 100644
index 00000000..26b3b954
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.2.11 on 2020-05-27 07:17
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0051_create_news_setting'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='user',
+ name='avatar_hash',
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py
deleted file mode 100644
index b51ce1d2..00000000
--- a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 2.2.11 on 2020-03-30 10:24
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0052_create_news_setting'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='offtopicchannelname',
- name='used',
- field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py
new file mode 100644
index 00000000..7ff3a548
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.2.11 on 2020-06-02 13:42
+
+import django.contrib.postgres.fields
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0052_remove_user_avatar_hash'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='user',
+ name='roles',
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='roles',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py
deleted file mode 100644
index be9fd948..00000000
--- a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 2.2.11 on 2020-05-27 07:17
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0053_offtopicchannelname_used'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='user',
- name='avatar_hash',
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py
new file mode 100644
index 00000000..96230015
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py
@@ -0,0 +1,21 @@
+# Generated by Django 2.2.11 on 2020-06-02 20:08
+
+import django.contrib.postgres.fields
+import django.core.validators
+from django.db import migrations, models
+import pydis_site.apps.api.models.bot.user
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0053_user_roles_to_array'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='roles',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py
new file mode 100644
index 00000000..f2a0e638
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.8 on 2020-07-14 20:27
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0051_allow_blank_message_embeds'),
+ ('api', '0054_user_invalidate_unknown_role'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py
new file mode 100644
index 00000000..d73b450d
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2.14 on 2020-07-15 07:37
+
+import django.contrib.postgres.fields
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0054_user_invalidate_unknown_role'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='reminder',
+ name='mentions',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py
deleted file mode 100644
index e7b4a983..00000000
--- a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 2.2.11 on 2020-06-02 13:42
-
-import django.contrib.postgres.fields
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0054_remove_user_avatar_hash'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='user',
- name='roles',
- ),
- migrations.AddField(
- model_name='user',
- name='roles',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py
new file mode 100644
index 00000000..489941c7
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.0.8 on 2020-07-14 20:35
+
+import django.contrib.postgres.fields
+import django.core.validators
+from django.db import migrations, models
+import pydis_site.apps.api.models.bot.user
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0055_merge_20200714_2027'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='roles',
+ field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py
deleted file mode 100644
index ab2696aa..00000000
--- a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 2.2.11 on 2020-06-02 20:08
-
-import django.contrib.postgres.fields
-import django.core.validators
-from django.db import migrations, models
-import pydis_site.apps.api.models.bot.user
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0055_user_roles_to_array'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='user',
- name='roles',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py
new file mode 100644
index 00000000..47a6d2d4
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.14 on 2020-07-16 07:51
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0055_reminder_mentions'),
+ ('api', '0056_allow_blank_user_roles'),
+ ]
+
+ operations = [
+ ]
diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py
deleted file mode 100644
index fb829a17..00000000
--- a/pydis_site/apps/api/migrations/0057_reminder_mentions.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 2.2.14 on 2020-07-15 07:37
-
-import django.contrib.postgres.fields
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0056_user_invalidate_unknown_role'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='reminder',
- name='mentions',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py
deleted file mode 100644
index 8f7fddfc..00000000
--- a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 3.0.8 on 2020-07-14 20:35
-
-import django.contrib.postgres.fields
-import django.core.validators
-from django.db import migrations, models
-import pydis_site.apps.api.models.bot.user
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0057_reminder_mentions'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='user',
- name='roles',
- field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None),
- ),
- ]
diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py
new file mode 100644
index 00000000..aecfdad7
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.0.8 on 2020-07-15 11:23
+
+from django.db import migrations, models
+import pydis_site.apps.api.models.mixins
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('api', '0057_merge_20200716_0751'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='FilterList',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('type', models.CharField(
+ choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'),
+ ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')],
+ help_text='The type of allowlist this is on.', max_length=50)),
+ ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')),
+ ('content', models.TextField(help_text='The data to add to the allow or denylist.')),
+ ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)),
+ ],
+ bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
+ ),
+ migrations.AddConstraint(
+ model_name='filterlist',
+ constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list')
+ )
+ ]
diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py
deleted file mode 100644
index eac5542d..00000000
--- a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Generated by Django 3.0.8 on 2020-07-15 11:23
-
-from django.db import migrations, models
-import pydis_site.apps.api.models.mixins
-
-
-class Migration(migrations.Migration):
- dependencies = [
- ('api', '0058_allow_blank_user_roles'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='FilterList',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('type', models.CharField(
- choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'),
- ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')],
- help_text='The type of allowlist this is on.', max_length=50)),
- ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')),
- ('content', models.TextField(help_text='The data to add to the allow or denylist.')),
- ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)),
- ],
- bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
- ),
- migrations.AddConstraint(
- model_name='filterlist',
- constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list')
- )
- ]
diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py
new file mode 100644
index 00000000..8c550191
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py
@@ -0,0 +1,153 @@
+from django.db import migrations
+
+guild_invite_whitelist = [
+ ("discord.gg/python", "Python Discord", True),
+ ("discord.gg/4JJdJKb", "RLBot", True),
+ ("discord.gg/djPtTRJ", "Kivy", True),
+ ("discord.gg/QXyegWe", "Pyglet", True),
+ ("discord.gg/9XsucTT", "Panda3D", True),
+ ("discord.gg/AP3rq2k", "PyWeek", True),
+ ("discord.gg/vSPsP9t", "Microsoft Python", True),
+ ("discord.gg/bRCvFy9", "Discord.js Official", True),
+ ("discord.gg/9zT7NHP", "Programming Discussions", True),
+ ("discord.gg/ysd6M4r", "JetBrains Community", True),
+ ("discord.gg/4xJeCgy", "Raspberry Pie", True),
+ ("discord.gg/AStb3kZ", "Ren'Py", True),
+ ("discord.gg/t655QNV", "Python Discord: Emojis 1", True),
+ ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True),
+ ("discord.gg/jTtgWuy", "Django", True),
+ ("discord.gg/W9BypZF", "STEM", True),
+ ("discord.gg/dpy", "discord.py", True),
+ ("discord.gg/programming", "Programmers Hangout", True),
+ ("discord.gg/qhGUjGD", "SpeakJS", True),
+ ("discord.gg/eTbWSZj", "Functional Programming", True),
+ ("discord.gg/r8yreB6", "PyGame", True),
+ ("discord.gg/5UBnR3P", "Python Atlanta", True),
+ ("discord.gg/ccyrDKv", "C#", True),
+]
+
+domain_name_blacklist = [
+ ("pornhub.com", None, False),
+ ("liveleak.com", None, False),
+ ("grabify.link", None, False),
+ ("bmwforum.co", None, False),
+ ("leancoding.co", None, False),
+ ("spottyfly.com", None, False),
+ ("stopify.co", None, False),
+ ("yoütu.be", None, False),
+ ("discörd.com", None, False),
+ ("minecräft.com", None, False),
+ ("freegiftcards.co", None, False),
+ ("disçordapp.com", None, False),
+ ("fortnight.space", None, False),
+ ("fortnitechat.site", None, False),
+ ("joinmy.site", None, False),
+ ("curiouscat.club", None, False),
+ ("catsnthings.fun", None, False),
+ ("yourtube.site", None, False),
+ ("youtubeshort.watch", None, False),
+ ("catsnthing.com", None, False),
+ ("youtubeshort.pro", None, False),
+ ("canadianlumberjacks.online", None, False),
+ ("poweredbydialup.club", None, False),
+ ("poweredbydialup.online", None, False),
+ ("poweredbysecurity.org", None, False),
+ ("poweredbysecurity.online", None, False),
+ ("ssteam.site", None, False),
+ ("steamwalletgift.com", None, False),
+ ("discord.gift", None, False),
+ ("lmgtfy.com", None, False),
+]
+
+filter_token_blacklist = [
+ ("\bgoo+ks*\b", None, False),
+ ("\bky+s+\b", None, False),
+ ("\bki+ke+s*\b", None, False),
+ ("\bbeaner+s?\b", None, False),
+ ("\bcoo+ns*\b", None, False),
+ ("\bnig+lets*\b", None, False),
+ ("\bslant-eyes*\b", None, False),
+ ("\btowe?l-?head+s*\b", None, False),
+ ("\bchi*n+k+s*\b", None, False),
+ ("\bspick*s*\b", None, False),
+ ("\bkill* +(?:yo)?urself+\b", None, False),
+ ("\bjew+s*\b", None, False),
+ ("\bsuicide\b", None, False),
+ ("\brape\b", None, False),
+ ("\b(re+)tar+(d+|t+)(ed)?\b", None, False),
+ ("\bta+r+d+\b", None, False),
+ ("\bcunts*\b", None, False),
+ ("\btrann*y\b", None, False),
+ ("\bshemale\b", None, False),
+ ("fa+g+s*", None, False),
+ ("卐", None, False),
+ ("卍", None, False),
+ ("࿖", None, False),
+ ("࿕", None, False),
+ ("࿘", None, False),
+ ("࿗", None, False),
+ ("cuck(?!oo+)", None, False),
+ ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False),
+ ("fag+o+t+s*", None, False),
+]
+
+file_format_whitelist = [
+ (".3gp", None, True),
+ (".3g2", None, True),
+ (".avi", None, True),
+ (".bmp", None, True),
+ (".gif", None, True),
+ (".h264", None, True),
+ (".jpg", None, True),
+ (".jpeg", None, True),
+ (".m4v", None, True),
+ (".mkv", None, True),
+ (".mov", None, True),
+ (".mp4", None, True),
+ (".mpeg", None, True),
+ (".mpg", None, True),
+ (".png", None, True),
+ (".tiff", None, True),
+ (".wmv", None, True),
+ (".svg", None, True),
+ (".psd", "Photoshop", True),
+ (".ai", "Illustrator", True),
+ (".aep", "After Effects", True),
+ (".xcf", "GIMP", True),
+ (".mp3", None, True),
+ (".wav", None, True),
+ (".ogg", None, True),
+ (".webm", None, True),
+ (".webp", None, True),
+]
+
+populate_data = {
+ "FILTER_TOKEN": filter_token_blacklist,
+ "DOMAIN_NAME": domain_name_blacklist,
+ "FILE_FORMAT": file_format_whitelist,
+ "GUILD_INVITE": guild_invite_whitelist,
+}
+
+
+class Migration(migrations.Migration):
+ dependencies = [("api", "0058_create_new_filterlist_model")]
+
+ def populate_filterlists(app, _):
+ FilterList = app.get_model("api", "FilterList")
+
+ for filterlist_type, metadata in populate_data.items():
+ for content, comment, allowed in metadata:
+ FilterList.objects.create(
+ type=filterlist_type,
+ allowed=allowed,
+ content=content,
+ comment=comment,
+ )
+
+ def clear_filterlists(app, _):
+ FilterList = app.get_model("api", "FilterList")
+ FilterList.objects.all().delete()
+
+ operations = [
+ migrations.RunPython(populate_filterlists, clear_filterlists)
+ ]
diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py
deleted file mode 100644
index 35fde95a..00000000
--- a/pydis_site/apps/api/migrations/0060_populate_filterlists.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from django.db import migrations
-
-guild_invite_whitelist = [
- ("discord.gg/python", "Python Discord", True),
- ("discord.gg/4JJdJKb", "RLBot", True),
- ("discord.gg/djPtTRJ", "Kivy", True),
- ("discord.gg/QXyegWe", "Pyglet", True),
- ("discord.gg/9XsucTT", "Panda3D", True),
- ("discord.gg/AP3rq2k", "PyWeek", True),
- ("discord.gg/vSPsP9t", "Microsoft Python", True),
- ("discord.gg/bRCvFy9", "Discord.js Official", True),
- ("discord.gg/9zT7NHP", "Programming Discussions", True),
- ("discord.gg/ysd6M4r", "JetBrains Community", True),
- ("discord.gg/4xJeCgy", "Raspberry Pie", True),
- ("discord.gg/AStb3kZ", "Ren'Py", True),
- ("discord.gg/t655QNV", "Python Discord: Emojis 1", True),
- ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True),
- ("discord.gg/jTtgWuy", "Django", True),
- ("discord.gg/W9BypZF", "STEM", True),
- ("discord.gg/dpy", "discord.py", True),
- ("discord.gg/programming", "Programmers Hangout", True),
- ("discord.gg/qhGUjGD", "SpeakJS", True),
- ("discord.gg/eTbWSZj", "Functional Programming", True),
- ("discord.gg/r8yreB6", "PyGame", True),
- ("discord.gg/5UBnR3P", "Python Atlanta", True),
- ("discord.gg/ccyrDKv", "C#", True),
-]
-
-domain_name_blacklist = [
- ("pornhub.com", None, False),
- ("liveleak.com", None, False),
- ("grabify.link", None, False),
- ("bmwforum.co", None, False),
- ("leancoding.co", None, False),
- ("spottyfly.com", None, False),
- ("stopify.co", None, False),
- ("yoütu.be", None, False),
- ("discörd.com", None, False),
- ("minecräft.com", None, False),
- ("freegiftcards.co", None, False),
- ("disçordapp.com", None, False),
- ("fortnight.space", None, False),
- ("fortnitechat.site", None, False),
- ("joinmy.site", None, False),
- ("curiouscat.club", None, False),
- ("catsnthings.fun", None, False),
- ("yourtube.site", None, False),
- ("youtubeshort.watch", None, False),
- ("catsnthing.com", None, False),
- ("youtubeshort.pro", None, False),
- ("canadianlumberjacks.online", None, False),
- ("poweredbydialup.club", None, False),
- ("poweredbydialup.online", None, False),
- ("poweredbysecurity.org", None, False),
- ("poweredbysecurity.online", None, False),
- ("ssteam.site", None, False),
- ("steamwalletgift.com", None, False),
- ("discord.gift", None, False),
- ("lmgtfy.com", None, False),
-]
-
-filter_token_blacklist = [
- ("\bgoo+ks*\b", None, False),
- ("\bky+s+\b", None, False),
- ("\bki+ke+s*\b", None, False),
- ("\bbeaner+s?\b", None, False),
- ("\bcoo+ns*\b", None, False),
- ("\bnig+lets*\b", None, False),
- ("\bslant-eyes*\b", None, False),
- ("\btowe?l-?head+s*\b", None, False),
- ("\bchi*n+k+s*\b", None, False),
- ("\bspick*s*\b", None, False),
- ("\bkill* +(?:yo)?urself+\b", None, False),
- ("\bjew+s*\b", None, False),
- ("\bsuicide\b", None, False),
- ("\brape\b", None, False),
- ("\b(re+)tar+(d+|t+)(ed)?\b", None, False),
- ("\bta+r+d+\b", None, False),
- ("\bcunts*\b", None, False),
- ("\btrann*y\b", None, False),
- ("\bshemale\b", None, False),
- ("fa+g+s*", None, False),
- ("卐", None, False),
- ("卍", None, False),
- ("࿖", None, False),
- ("࿕", None, False),
- ("࿘", None, False),
- ("࿗", None, False),
- ("cuck(?!oo+)", None, False),
- ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False),
- ("fag+o+t+s*", None, False),
-]
-
-file_format_whitelist = [
- (".3gp", None, True),
- (".3g2", None, True),
- (".avi", None, True),
- (".bmp", None, True),
- (".gif", None, True),
- (".h264", None, True),
- (".jpg", None, True),
- (".jpeg", None, True),
- (".m4v", None, True),
- (".mkv", None, True),
- (".mov", None, True),
- (".mp4", None, True),
- (".mpeg", None, True),
- (".mpg", None, True),
- (".png", None, True),
- (".tiff", None, True),
- (".wmv", None, True),
- (".svg", None, True),
- (".psd", "Photoshop", True),
- (".ai", "Illustrator", True),
- (".aep", "After Effects", True),
- (".xcf", "GIMP", True),
- (".mp3", None, True),
- (".wav", None, True),
- (".ogg", None, True),
- (".webm", None, True),
- (".webp", None, True),
-]
-
-populate_data = {
- "FILTER_TOKEN": filter_token_blacklist,
- "DOMAIN_NAME": domain_name_blacklist,
- "FILE_FORMAT": file_format_whitelist,
- "GUILD_INVITE": guild_invite_whitelist,
-}
-
-
-class Migration(migrations.Migration):
- dependencies = [("api", "0059_create_new_filterlist_model")]
-
- def populate_filterlists(app, _):
- FilterList = app.get_model("api", "FilterList")
-
- for filterlist_type, metadata in populate_data.items():
- for content, comment, allowed in metadata:
- FilterList.objects.create(
- type=filterlist_type,
- allowed=allowed,
- content=content,
- comment=comment,
- )
-
- def clear_filterlists(app, _):
- FilterList = app.get_model("api", "FilterList")
- FilterList.objects.all().delete()
-
- operations = [
- migrations.RunPython(populate_filterlists, clear_filterlists)
- ]
diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py
new file mode 100644
index 00000000..53846f02
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py
@@ -0,0 +1,85 @@
+from django.db import migrations
+
+bad_guild_invite_whitelist = [
+ ("discord.gg/python", "Python Discord", True),
+ ("discord.gg/4JJdJKb", "RLBot", True),
+ ("discord.gg/djPtTRJ", "Kivy", True),
+ ("discord.gg/QXyegWe", "Pyglet", True),
+ ("discord.gg/9XsucTT", "Panda3D", True),
+ ("discord.gg/AP3rq2k", "PyWeek", True),
+ ("discord.gg/vSPsP9t", "Microsoft Python", True),
+ ("discord.gg/bRCvFy9", "Discord.js Official", True),
+ ("discord.gg/9zT7NHP", "Programming Discussions", True),
+ ("discord.gg/ysd6M4r", "JetBrains Community", True),
+ ("discord.gg/4xJeCgy", "Raspberry Pie", True),
+ ("discord.gg/AStb3kZ", "Ren'Py", True),
+ ("discord.gg/t655QNV", "Python Discord: Emojis 1", True),
+ ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True),
+ ("discord.gg/jTtgWuy", "Django", True),
+ ("discord.gg/W9BypZF", "STEM", True),
+ ("discord.gg/dpy", "discord.py", True),
+ ("discord.gg/programming", "Programmers Hangout", True),
+ ("discord.gg/qhGUjGD", "SpeakJS", True),
+ ("discord.gg/eTbWSZj", "Functional Programming", True),
+ ("discord.gg/r8yreB6", "PyGame", True),
+ ("discord.gg/5UBnR3P", "Python Atlanta", True),
+ ("discord.gg/ccyrDKv", "C#", True),
+]
+
+guild_invite_whitelist = [
+ ("267624335836053506", "Python Discord", True),
+ ("348658686962696195", "RLBot", True),
+ ("423249981340778496", "Kivy", True),
+ ("438622377094414346", "Pyglet", True),
+ ("524691714909274162", "Panda3D", True),
+ ("666560367173828639", "PyWeek", True),
+ ("702724176489873509", "Microsoft Python", True),
+ ("222078108977594368", "Discord.js Official", True),
+ ("238666723824238602", "Programming Discussions", True),
+ ("433980600391696384", "JetBrains Community", True),
+ ("204621105720328193", "Raspberry Pie", True),
+ ("286633898581164032", "Ren'Py", True),
+ ("440186186024222721", "Python Discord: Emojis 1", True),
+ ("578587418123304970", "Python Discord: Emojis 2", True),
+ ("159039020565790721", "Django", True),
+ ("273944235143593984", "STEM", True),
+ ("336642139381301249", "discord.py", True),
+ ("244230771232079873", "Programmers Hangout", True),
+ ("239433591950540801", "SpeakJS", True),
+ ("280033776820813825", "Functional Programming", True),
+ ("349505959032389632", "PyGame", True),
+ ("488751051629920277", "Python Atlanta", True),
+ ("143867839282020352", "C#", True),
+]
+
+
+class Migration(migrations.Migration):
+ dependencies = [("api", "0059_populate_filterlists")]
+
+ def fix_filterlist(app, _):
+ FilterList = app.get_model("api", "FilterList")
+ FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059.
+
+ for content, comment, allowed in guild_invite_whitelist:
+ FilterList.objects.create(
+ type="GUILD_INVITE",
+ allowed=allowed,
+ content=content,
+ comment=comment,
+ )
+
+ def restore_bad_filterlist(app, _):
+ FilterList = app.get_model("api", "FilterList")
+ FilterList.objects.filter(type="GUILD_INVITE").delete()
+
+ for content, comment, allowed in bad_guild_invite_whitelist:
+ FilterList.objects.create(
+ type="GUILD_INVITE",
+ allowed=allowed,
+ content=content,
+ comment=comment,
+ )
+
+ operations = [
+ migrations.RunPython(fix_filterlist, restore_bad_filterlist)
+ ]
diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py
deleted file mode 100644
index eaaafb38..00000000
--- a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py
+++ /dev/null
@@ -1,85 +0,0 @@
-from django.db import migrations
-
-bad_guild_invite_whitelist = [
- ("discord.gg/python", "Python Discord", True),
- ("discord.gg/4JJdJKb", "RLBot", True),
- ("discord.gg/djPtTRJ", "Kivy", True),
- ("discord.gg/QXyegWe", "Pyglet", True),
- ("discord.gg/9XsucTT", "Panda3D", True),
- ("discord.gg/AP3rq2k", "PyWeek", True),
- ("discord.gg/vSPsP9t", "Microsoft Python", True),
- ("discord.gg/bRCvFy9", "Discord.js Official", True),
- ("discord.gg/9zT7NHP", "Programming Discussions", True),
- ("discord.gg/ysd6M4r", "JetBrains Community", True),
- ("discord.gg/4xJeCgy", "Raspberry Pie", True),
- ("discord.gg/AStb3kZ", "Ren'Py", True),
- ("discord.gg/t655QNV", "Python Discord: Emojis 1", True),
- ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True),
- ("discord.gg/jTtgWuy", "Django", True),
- ("discord.gg/W9BypZF", "STEM", True),
- ("discord.gg/dpy", "discord.py", True),
- ("discord.gg/programming", "Programmers Hangout", True),
- ("discord.gg/qhGUjGD", "SpeakJS", True),
- ("discord.gg/eTbWSZj", "Functional Programming", True),
- ("discord.gg/r8yreB6", "PyGame", True),
- ("discord.gg/5UBnR3P", "Python Atlanta", True),
- ("discord.gg/ccyrDKv", "C#", True),
-]
-
-guild_invite_whitelist = [
- ("267624335836053506", "Python Discord", True),
- ("348658686962696195", "RLBot", True),
- ("423249981340778496", "Kivy", True),
- ("438622377094414346", "Pyglet", True),
- ("524691714909274162", "Panda3D", True),
- ("666560367173828639", "PyWeek", True),
- ("702724176489873509", "Microsoft Python", True),
- ("222078108977594368", "Discord.js Official", True),
- ("238666723824238602", "Programming Discussions", True),
- ("433980600391696384", "JetBrains Community", True),
- ("204621105720328193", "Raspberry Pie", True),
- ("286633898581164032", "Ren'Py", True),
- ("440186186024222721", "Python Discord: Emojis 1", True),
- ("578587418123304970", "Python Discord: Emojis 2", True),
- ("159039020565790721", "Django", True),
- ("273944235143593984", "STEM", True),
- ("336642139381301249", "discord.py", True),
- ("244230771232079873", "Programmers Hangout", True),
- ("239433591950540801", "SpeakJS", True),
- ("280033776820813825", "Functional Programming", True),
- ("349505959032389632", "PyGame", True),
- ("488751051629920277", "Python Atlanta", True),
- ("143867839282020352", "C#", True),
-]
-
-
-class Migration(migrations.Migration):
- dependencies = [("api", "0060_populate_filterlists")]
-
- def fix_filterlist(app, _):
- FilterList = app.get_model("api", "FilterList")
- FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059.
-
- for content, comment, allowed in guild_invite_whitelist:
- FilterList.objects.create(
- type="GUILD_INVITE",
- allowed=allowed,
- content=content,
- comment=comment,
- )
-
- def restore_bad_filterlist(app, _):
- FilterList = app.get_model("api", "FilterList")
- FilterList.objects.filter(type="GUILD_INVITE").delete()
-
- for content, comment, allowed in bad_guild_invite_whitelist:
- FilterList.objects.create(
- type="GUILD_INVITE",
- allowed=allowed,
- content=content,
- comment=comment,
- )
-
- operations = [
- migrations.RunPython(fix_filterlist, restore_bad_filterlist)
- ]
--
cgit v1.2.3
From ab634c6ef1d4b7cec3b8471262eec3ec727478d8 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sun, 30 Aug 2020 08:27:07 +0300
Subject: Create merge migration to fix conflicts
---
pydis_site/apps/api/migrations/0061_merge_20200830_0526.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 pydis_site/apps/api/migrations/0061_merge_20200830_0526.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py
new file mode 100644
index 00000000..f0668696
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.8 on 2020-08-30 05:26
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0060_populate_filterlists_fix'),
+ ('api', '0052_offtopicchannelname_used'),
+ ]
+
+ operations = [
+ ]
--
cgit v1.2.3
From a9e1f6f398ca4cac42c01a211513c02881e7541d Mon Sep 17 00:00:00 2001
From: Karlis S
Date: Tue, 1 Sep 2020 14:59:23 +0000
Subject: Create new merge migration to fix conflicts between migrations
---
pydis_site/apps/api/migrations/0062_merge_20200901_1459.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 pydis_site/apps/api/migrations/0062_merge_20200901_1459.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py
new file mode 100644
index 00000000..d162acf1
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.8 on 2020-09-01 14:59
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0051_delete_tag'),
+ ('api', '0061_merge_20200830_0526'),
+ ]
+
+ operations = [
+ ]
--
cgit v1.2.3
From 01a0e825333812dff21351201fce8c25031cc8fa Mon Sep 17 00:00:00 2001
From: Leon Sandøy
Date: Thu, 3 Sep 2020 00:39:25 +0200
Subject: Update landing page.
Replace flake8-annotations with metricity.
---
pydis_site/apps/home/tests/mock_github_api_response.json | 2 +-
pydis_site/apps/home/views/home.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json
index 35604a85..10be4f99 100644
--- a/pydis_site/apps/home/tests/mock_github_api_response.json
+++ b/pydis_site/apps/home/tests/mock_github_api_response.json
@@ -28,7 +28,7 @@
"forks_count": 31
},
{
- "full_name": "python-discord/flake8-annotations",
+ "full_name": "python-discord/metricity",
"description": "test",
"stargazers_count": 97,
"language": "Python",
diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py
index 20e38ab0..3b5cd5ac 100644
--- a/pydis_site/apps/home/views/home.py
+++ b/pydis_site/apps/home/views/home.py
@@ -23,7 +23,7 @@ class HomeView(View):
"python-discord/bot",
"python-discord/snekbox",
"python-discord/seasonalbot",
- "python-discord/flake8-annotations",
+ "python-discord/metricity",
"python-discord/django-simple-bulma",
]
--
cgit v1.2.3
From 49e7243f6527140dbdb29a2f1dd994839f248dba Mon Sep 17 00:00:00 2001
From: Eivind Teig
Date: Fri, 11 Sep 2020 23:43:06 +0200
Subject: Allow blank/null input to the nomination reason.
We are lowering the threshold for nomination. By allowing the users to
make a nomination without a reason might make this feature more
attractive amongst members of staff.
---
.../0063_Allow_blank_or_null_for_nomination_reason.py | 18 ++++++++++++++++++
pydis_site/apps/api/models/bot/nomination.py | 4 +++-
2 files changed, 21 insertions(+), 1 deletion(-)
create mode 100644 pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py
new file mode 100644
index 00000000..9eb05eaa
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.9 on 2020-09-11 21:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0062_merge_20200901_1459'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='nomination',
+ name='reason',
+ field=models.TextField(blank=True, help_text='Why this user was nominated.', null=True),
+ ),
+ ]
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index 21e34e87..183b22d5 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -18,7 +18,9 @@ class Nomination(ModelReprMixin, models.Model):
related_name='nomination_set'
)
reason = models.TextField(
- help_text="Why this user was nominated."
+ help_text="Why this user was nominated.",
+ null=True,
+ blank=True
)
user = models.ForeignKey(
User,
--
cgit v1.2.3
From 1ed224135e48d5d1840d14d446575b8cbc5a7a6e Mon Sep 17 00:00:00 2001
From: Eivind Teig
Date: Sat, 12 Sep 2020 00:13:32 +0200
Subject: Fix a broken test for nomination reason.
We no longer return 400 if a reason is missing.
---
pydis_site/apps/api/tests/test_nominations.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
index 92c62c87..b37135f8 100644
--- a/pydis_site/apps/api/tests/test_nominations.py
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -80,7 +80,7 @@ class CreationTests(APISubdomainTestCase):
'actor': ['This field is required.']
})
- def test_returns_400_for_missing_reason(self):
+ def test_returns_201_for_missing_reason(self):
url = reverse('bot:nomination-list', host='api')
data = {
'user': self.user.id,
@@ -88,10 +88,7 @@ class CreationTests(APISubdomainTestCase):
}
response = self.client.post(url, data=data)
- self.assertEqual(response.status_code, 400)
- self.assertEqual(response.json(), {
- 'reason': ['This field is required.']
- })
+ self.assertEqual(response.status_code, 201)
def test_returns_400_for_bad_user(self):
url = reverse('bot:nomination-list', host='api')
--
cgit v1.2.3
From 2029cf511ee61d48eca94811e6e891a6c827de7e Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 04:30:05 +1000
Subject: Remove TagAdmin model as Tags were removed from site.
---
pydis_site/apps/api/admin.py | 23 -----------------------
1 file changed, 23 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 271ff119..4bf2a6ee 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -242,29 +242,6 @@ class RoleAdmin(admin.ModelAdmin):
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."""
--
cgit v1.2.3
From 4ad759c21ca91bc38727c27083e8aa48b30b7325 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 11:46:53 +1000
Subject: Use admin.register decorators.
---
pydis_site/apps/api/admin.py | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 4bf2a6ee..6116fbf8 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -22,6 +22,7 @@ from .models import (
)
+@admin.register(LogEntry)
class LogEntryAdmin(admin.ModelAdmin):
"""Allows viewing logs in the Django Admin without allowing edits."""
@@ -53,6 +54,7 @@ class LogEntryAdmin(admin.ModelAdmin):
return False
+@admin.register(DeletedMessage)
class DeletedMessageAdmin(admin.ModelAdmin):
"""Admin formatting for the DeletedMessage model."""
@@ -109,6 +111,7 @@ class DeletedMessageAdmin(admin.ModelAdmin):
)
+@admin.register(MessageDeletionContext)
class MessageDeletionContextAdmin(admin.ModelAdmin):
"""Admin formatting for the MessageDeletionContext model."""
@@ -123,6 +126,7 @@ class MessageDeletionContextAdmin(admin.ModelAdmin):
)
+@admin.register(Infraction)
class InfractionAdmin(admin.ModelAdmin):
"""Admin formatting for the Infraction model."""
@@ -167,6 +171,7 @@ class InfractionAdmin(admin.ModelAdmin):
)
+@admin.register(Nomination)
class NominationAdmin(admin.ModelAdmin):
"""Admin formatting for the Nomination model."""
@@ -204,12 +209,14 @@ class NominationAdmin(admin.ModelAdmin):
list_filter = ("active",)
+@admin.register(OffTopicChannelName)
class OffTopicChannelNameAdmin(admin.ModelAdmin):
"""Admin formatting for the OffTopicChannelName model."""
search_fields = ("name",)
+@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
"""Admin formatting for the Role model."""
@@ -266,6 +273,7 @@ class StaffRolesFilter(admin.SimpleListFilter):
return queryset.filter(roles__name=value)
+@admin.register(User)
class UserAdmin(admin.ModelAdmin):
"""Admin formatting for the User model."""
@@ -283,13 +291,5 @@ class UserAdmin(admin.ModelAdmin):
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(OffensiveMessage)
-admin.site.register(OffTopicChannelName, OffTopicChannelNameAdmin)
-admin.site.register(Role, RoleAdmin)
-admin.site.register(User, UserAdmin)
--
cgit v1.2.3
From a311ad08003203a5aa38bf49fec69d6d42f74439 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 11:48:54 +1000
Subject: Update UserAdmin to use new role values, pretty colours.
---
pydis_site/apps/api/admin.py | 78 ++++++++++++++++++++--------------
pydis_site/apps/api/models/bot/user.py | 4 ++
2 files changed, 50 insertions(+), 32 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 6116fbf8..6101f88c 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -1,9 +1,10 @@
+from __future__ import annotations
+
import json
-from typing import Optional, Tuple
+from typing import Optional
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
@@ -249,45 +250,58 @@ class RoleAdmin(admin.ModelAdmin):
permissions_with_calc_link.short_description = "Permissions"
-class StaffRolesFilter(admin.SimpleListFilter):
- """Filter options for Staff Roles."""
+class UserTopRoleFilter(admin.SimpleListFilter):
+ """List Filter for User list Admin page."""
- title = "Staff Role"
- parameter_name = "staff_role"
+ title = "Role"
+ parameter_name = "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 lookups(self, request, model_admin: UserAdmin):
+ """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]:
- """Returned data filter based on selected option."""
- value = self.value()
- if value:
- return queryset.filter(roles__name=value)
+ def queryset(self, request, queryset):
+ if not self.value():
+ return
+ role = Role.objects.get(name=self.value())
+ return queryset.filter(roles__contains=[role.id])
@admin.register(User)
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",
- )
+ def top_role_coloured(self, obj: User):
+ """Returns the top role of the user with html style matching role colour."""
+ return format_html(
+ f'{obj.top_role.name}'
+ )
+
+ top_role_coloured.short_description = "Top Role"
+
+ def all_roles_coloured(self, obj: User):
+ """Returns all user roles with html style matching role colours."""
+ roles = Role.objects.filter(id__in=obj.roles)
+ return format_html(
+ "".join(
+ f'{r.name}' for r in roles
+ )
+ )
+
+ all_roles_coloured.short_description = "All Roles"
+
+ search_fields = ("name", "id", "roles")
+ list_filter = (UserTopRoleFilter, "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, request):
+ return False
+
+ def has_change_permission(self, request, obj=None):
+ return False
admin.site.register(BotSetting)
diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py
index cd2d58b9..70a30449 100644
--- a/pydis_site/apps/api/models/bot/user.py
+++ b/pydis_site/apps/api/models/bot/user.py
@@ -75,3 +75,7 @@ class User(ModelReprMixin, models.Model):
if not roles:
return Role.objects.get(name="Developers")
return max(roles)
+
+ @property
+ def username(self):
+ return f"{self.name}#{self.discriminator:04d}"
--
cgit v1.2.3
From dfbce19d39b0b6243865e682b8e6d86b8a028a71 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 12:00:13 +1000
Subject: Add verbose names for user fields that need capitalisation fixes.
---
pydis_site/apps/api/models/bot/user.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py
index 70a30449..a8604001 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,7 +58,8 @@ 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):
--
cgit v1.2.3
From 77c9e3ffce7992706202cf20ae3addf42c4dbf6c Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 12:40:24 +1000
Subject: Add return types and docstrings for new user admin changes.
---
pydis_site/apps/api/admin.py | 29 ++++++++++++++++++-----------
pydis_site/apps/api/models/bot/user.py | 11 ++++++++---
2 files changed, 26 insertions(+), 14 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 6101f88c..d77ae620 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -1,12 +1,13 @@
from __future__ import annotations
import json
-from typing import Optional
+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 format_html
+from django.utils.html import SafeString, format_html
from .models import (
BotSetting,
@@ -256,12 +257,13 @@ class UserTopRoleFilter(admin.SimpleListFilter):
title = "Role"
parameter_name = "role"
- def lookups(self, request, model_admin: UserAdmin):
+ def lookups(self, request: HttpRequest, model_admin: 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, queryset):
+ 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())
@@ -272,20 +274,23 @@ class UserTopRoleFilter(admin.SimpleListFilter):
class UserAdmin(admin.ModelAdmin):
"""Admin formatting for the User model."""
- def top_role_coloured(self, obj: User):
+ def top_role_coloured(self, user: User) -> SafeString:
"""Returns the top role of the user with html style matching role colour."""
return format_html(
- f'{obj.top_role.name}'
+ '{1}',
+ user.top_role.colour,
+ user.top_role.name
)
top_role_coloured.short_description = "Top Role"
- def all_roles_coloured(self, obj: User):
+ def all_roles_coloured(self, user: User) -> SafeString:
"""Returns all user roles with html style matching role colours."""
- roles = Role.objects.filter(id__in=obj.roles)
+ roles = Role.objects.filter(id__in=user.roles)
return format_html(
"".join(
- f'{r.name}' for r in roles
+ f'{r.name}'
+ for r in roles
)
)
@@ -297,10 +302,12 @@ class UserAdmin(admin.ModelAdmin):
fields = ("username", "id", "in_guild", "all_roles_coloured")
sortable_by = ("username",)
- def has_add_permission(self, request):
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
return False
- def has_change_permission(self, request, obj=None):
+ def has_change_permission(self, *args) -> bool:
+ """Prevent editing from django admin."""
return False
diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py
index a8604001..afc5ba1e 100644
--- a/pydis_site/apps/api/models/bot/user.py
+++ b/pydis_site/apps/api/models/bot/user.py
@@ -64,7 +64,7 @@ class User(ModelReprMixin, models.Model):
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:
@@ -79,5 +79,10 @@ class User(ModelReprMixin, models.Model):
return max(roles)
@property
- def username(self):
- return f"{self.name}#{self.discriminator:04d}"
+ 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)
--
cgit v1.2.3
From f132c485c7ecff661a257dcdf4b528b7300e95c1 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 12:45:25 +1000
Subject: Fix format-style not supporting X hex casting like f-strings.
---
pydis_site/apps/api/admin.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index d77ae620..15a1ec78 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -277,8 +277,8 @@ class UserAdmin(admin.ModelAdmin):
def top_role_coloured(self, user: User) -> SafeString:
"""Returns the top role of the user with html style matching role colour."""
return format_html(
- '{1}',
- user.top_role.colour,
+ '{1}',
+ f"#{user.top_role.colour:06X}",
user.top_role.name
)
--
cgit v1.2.3
From bbecf18704837f087e8254b657adf1c5e859dff9 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 12:47:32 +1000
Subject: Update Role ModelAdmin to past changes, cleanup formatting.
---
pydis_site/apps/api/admin.py | 45 ++++++++++++++++++++++------------
pydis_site/apps/api/models/bot/role.py | 3 ++-
2 files changed, 31 insertions(+), 17 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 15a1ec78..d32b4911 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -222,34 +222,47 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin):
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 coloured_name(self, role: Role) -> SafeString:
+ """Role name with html style colouring."""
+ return format_html(
+ '{1}',
+ f"#{role.colour:06X}",
+ role.name
+ )
+
+ coloured_name.short_description = "Name"
- def colour_with_preview(self, instance: Role) -> str:
+ def colour_with_preview(self, role: Role) -> SafeString:
"""Show colour value in both int and hex, in bolded and coloured style."""
return format_html(
- "{1} / #{0}",
- f"{instance.colour:06x}",
- instance.colour
+ "{0} ({1})",
+ f"#{role.colour:06x}",
+ role.colour
)
- def permissions_with_calc_link(self, instance: Role) -> str:
+ 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(
"{0}",
- instance.permissions
+ role.permissions
)
- colour_with_preview.short_description = "Colour"
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 UserTopRoleFilter(admin.SimpleListFilter):
"""List Filter for User list Admin page."""
diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py
index b23fc5f4..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,
--
cgit v1.2.3
From 8ed8b9ad62fd83e93d8dd5a136b7433f7240c4c2 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 13:01:49 +1000
Subject: Add OffensiveMessage Admin model.
---
pydis_site/apps/api/admin.py | 25 +++++++++++++++++++++-
.../apps/api/models/bot/offensive_message.py | 9 +++++---
2 files changed, 30 insertions(+), 4 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index d32b4911..59cab78d 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -218,6 +218,30 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin):
search_fields = ("name",)
+@admin.register(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(
+ '{1}',
+ 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
+
+
@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
"""Admin formatting for the Role model."""
@@ -326,4 +350,3 @@ class UserAdmin(admin.ModelAdmin):
admin.site.register(BotSetting)
admin.site.register(DocumentationLink)
-admin.site.register(OffensiveMessage)
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):
--
cgit v1.2.3
From c9671fb8fe8004c79139bca9a50ccc8ed1b3dffc Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 13:07:35 +1000
Subject: Add Used filter for OffTopicChannelName Admin model.
---
pydis_site/apps/api/admin.py | 1 +
1 file changed, 1 insertion(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 59cab78d..d07fc335 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -216,6 +216,7 @@ class OffTopicChannelNameAdmin(admin.ModelAdmin):
"""Admin formatting for the OffTopicChannelName model."""
search_fields = ("name",)
+ list_filter = ("used",)
@admin.register(OffensiveMessage)
--
cgit v1.2.3
From f4c47c976f72033f133bc90fcc8f30f291f3ad59 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 13:40:32 +1000
Subject: Update Nomination Admin model, add actor filter.
---
pydis_site/apps/api/admin.py | 47 ++++++++++++++++++++++++++++++++++----------
1 file changed, 37 insertions(+), 10 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index d07fc335..a85b4cac 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -173,18 +173,47 @@ class InfractionAdmin(admin.ModelAdmin):
)
+class NominationActorFilter(admin.SimpleListFilter):
+ """Actor Filter for Nomination Admin list page."""
+
+ title = "Actor"
+ parameter_name = "actor"
+
+ def lookups(self, request: HttpRequest, model_admin: 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())
+
+
@admin.register(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",
- "inserted_at",
- "ended_at"
)
+
fields = (
"user",
"active",
@@ -194,6 +223,8 @@ class NominationAdmin(admin.ModelAdmin):
"ended_at",
"end_reason"
)
+
+ # only allow reason fields to be edited.
readonly_fields = (
"user",
"active",
@@ -201,14 +232,10 @@ class NominationAdmin(admin.ModelAdmin):
"inserted_at",
"ended_at"
)
- search_fields = (
- "actor__name",
- "actor__id",
- "user__name",
- "user__id",
- "reason"
- )
- list_filter = ("active",)
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
@admin.register(OffTopicChannelName)
--
cgit v1.2.3
From 5c2e750b6ff19248aaafbb0a1c12f8c7523b7273 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 14:59:30 +1000
Subject: Update DeletedMessage and LogEntry Admin models, add verbose names
for Message
---
pydis_site/apps/api/admin.py | 167 ++++++++++++++++--------------
pydis_site/apps/api/models/bot/message.py | 6 +-
2 files changed, 95 insertions(+), 78 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index a85b4cac..ca97512f 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -23,34 +23,77 @@ from .models import (
User
)
+admin.site.site_header = "Python Discord | Administration"
+admin.site.site_title = "Python Discord"
+
+
+@admin.register(Infraction)
+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"
+ )
+
@admin.register(LogEntry)
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_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
@@ -60,7 +103,7 @@ class LogEntryAdmin(admin.ModelAdmin):
class DeletedMessageAdmin(admin.ModelAdmin):
"""Admin formatting for the DeletedMessage model."""
- readonly_fields = (
+ fields = (
"id",
"author",
"channel_id",
@@ -81,96 +124,68 @@ class DeletedMessageAdmin(admin.ModelAdmin):
"deletion_context__actor__id"
)
- @staticmethod
- def embed_data(instance: DeletedMessage) -> Optional[str]:
+ def embed_data(self, message: DeletedMessage) -> Optional[str]:
"""Format embed data in a code block for better readability."""
- if instance.embeds:
+ if message.embeds:
return format_html(
""
"{0}
",
- json.dumps(instance.embeds, indent=4)
+ json.dumps(message.embeds, indent=4)
)
+ embed_data.short_description = "Embeds"
+
@staticmethod
- def context(instance: DeletedMessage) -> str:
+ 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=[instance.deletion_context.id]
+ args=[message.deletion_context.id]
)
details = (
- f"Deleted by {instance.deletion_context.actor} at "
- f"{instance.deletion_context.creation}"
+ f"Deleted by {message.deletion_context.actor} at "
+ f"{message.deletion_context.creation}"
)
return format_html("{1}", link, details)
@staticmethod
- def view_full_log(instance: DeletedMessage) -> str:
+ def view_full_log(message: 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
+ 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
+
@admin.register(MessageDeletionContext)
class MessageDeletionContextAdmin(admin.ModelAdmin):
"""Admin formatting for the MessageDeletionContext model."""
- readonly_fields = ("actor", "creation", "message_log")
+ fields = ("actor", "creation")
+ list_display = ("id", "creation", "actor")
+ inlines = (DeletedMessageInline,)
- @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
- )
-
-
-@admin.register(Infraction)
-class InfractionAdmin(admin.ModelAdmin):
- """Admin formatting for the Infraction model."""
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
- 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"
- )
+ def has_change_permission(self, *args) -> bool:
+ """Prevent editing from django admin."""
+ return False
class NominationActorFilter(admin.SimpleListFilter):
@@ -179,7 +194,7 @@ class NominationActorFilter(admin.SimpleListFilter):
title = "Actor"
parameter_name = "actor"
- def lookups(self, request: HttpRequest, model_admin: NominationAdmin) -> Iterable[Tuple[int, str]]:
+ 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)
@@ -322,7 +337,7 @@ class UserTopRoleFilter(admin.SimpleListFilter):
title = "Role"
parameter_name = "role"
- def lookups(self, request: HttpRequest, model_admin: UserAdmin) -> Iterable[Tuple[str, str]]:
+ 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)
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,
--
cgit v1.2.3
From c6e80a97bb9e40dc404e274d6ebc419b410a2b66 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 15:16:00 +1000
Subject: Add DocumentationLink and BotSetting Admin models.
---
pydis_site/apps/api/admin.py | 32 ++++++++++++++++++++++++++++----
1 file changed, 28 insertions(+), 4 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index ca97512f..7b571005 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -27,6 +27,28 @@ admin.site.site_header = "Python Discord | Administration"
admin.site.site_title = "Python Discord"
+@admin.register(BotSetting)
+class BotSettingAdmin(admin.ModelAdmin):
+ """Admin formatting for the BotSetting model."""
+
+ fields = ("name", "data")
+ list_display = ("name",)
+
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
+
+@admin.register(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",)
+
+
@admin.register(Infraction)
class InfractionAdmin(admin.ModelAdmin):
"""Admin formatting for the Infraction model."""
@@ -71,6 +93,10 @@ class InfractionAdmin(admin.ModelAdmin):
"active"
)
+ def has_add_permission(self, *args) -> bool:
+ """Prevent adding from django admin."""
+ return False
+
@admin.register(LogEntry)
class LogEntryAdmin(admin.ModelAdmin):
@@ -124,6 +150,8 @@ class DeletedMessageAdmin(admin.ModelAdmin):
"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:
@@ -389,7 +417,3 @@ class UserAdmin(admin.ModelAdmin):
def has_change_permission(self, *args) -> bool:
"""Prevent editing from django admin."""
return False
-
-
-admin.site.register(BotSetting)
-admin.site.register(DocumentationLink)
--
cgit v1.2.3
From 8af9e190f69484efb5fe3b5910a9125738e0ee84 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 15:22:48 +1000
Subject: Declutter Infraction admin list, add actor list filter.
---
pydis_site/apps/api/admin.py | 30 +++++++++++++++++++++++++-----
1 file changed, 25 insertions(+), 5 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 7b571005..ff9afc46 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -49,6 +49,25 @@ class DocumentationLinkAdmin(admin.ModelAdmin):
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())
+
+
@admin.register(Infraction)
class InfractionAdmin(admin.ModelAdmin):
"""Admin formatting for the Infraction model."""
@@ -67,16 +86,16 @@ class InfractionAdmin(admin.ModelAdmin):
"user",
"actor",
"type",
- "inserted_at"
+ "inserted_at",
+ "active",
+ "hidden"
)
list_display = (
"type",
+ "active",
"user",
- "actor",
"inserted_at",
- "expires_at",
"reason",
- "active",
)
search_fields = (
"id",
@@ -90,7 +109,8 @@ class InfractionAdmin(admin.ModelAdmin):
list_filter = (
"type",
"hidden",
- "active"
+ "active",
+ InfractionActorFilter
)
def has_add_permission(self, *args) -> bool:
--
cgit v1.2.3
From 2c4b3451b3349d098ceebce774b08946b5e378d5 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Fri, 18 Sep 2020 15:24:54 +1000
Subject: Don't allow expiry to be editable, due to pending bot tasks unsyncing
---
pydis_site/apps/api/admin.py | 1 +
1 file changed, 1 insertion(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index ff9afc46..6267b7a8 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -87,6 +87,7 @@ class InfractionAdmin(admin.ModelAdmin):
"actor",
"type",
"inserted_at",
+ "expires_at",
"active",
"hidden"
)
--
cgit v1.2.3
From 8edeb88f92b8ed48f2f383c2245416486f8e99cc Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sat, 19 Sep 2020 01:23:58 +1000
Subject: Remove noqa from DeletedMessagesLogURLTests setup.
---
pydis_site/apps/api/tests/test_deleted_messages.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py
index 287c1737..448f1186 100644
--- a/pydis_site/apps/api/tests/test_deleted_messages.py
+++ b/pydis_site/apps/api/tests/test_deleted_messages.py
@@ -81,7 +81,7 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase):
class DeletedMessagesLogURLTests(APISubdomainTestCase):
@classmethod
- def setUpTestData(cls): # noqa
+ def setUpTestData(cls):
cls.author = cls.actor = User.objects.create(
id=324888,
name='Black Knight',
--
cgit v1.2.3
From 520ea636a3b22beb3fb153dea5e6dd0dd6f809f4 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sat, 19 Sep 2020 02:23:01 +1000
Subject: Update user model in DeletedMessagesLogURLTests.
`avatar_hash` is no longer a field stored in the database.
---
pydis_site/apps/api/tests/test_deleted_messages.py | 1 -
1 file changed, 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py
index 448f1186..40450844 100644
--- a/pydis_site/apps/api/tests/test_deleted_messages.py
+++ b/pydis_site/apps/api/tests/test_deleted_messages.py
@@ -86,7 +86,6 @@ class DeletedMessagesLogURLTests(APISubdomainTestCase):
id=324888,
name='Black Knight',
discriminator=1975,
- avatar_hash=None
)
cls.deletion_context = MessageDeletionContext.objects.create(
--
cgit v1.2.3
From 394d1ca151ad28be431eff8f25dde5707eeb0d47 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Sep 2020 00:04:20 +1000
Subject: Test username property formatting for user model.
---
pydis_site/apps/api/tests/test_users.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
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")
--
cgit v1.2.3
From dafca0fee8ac618ee9aab5ce8a1d7c34aafc3a05 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Sep 2020 02:06:11 +1000
Subject: Change UserTopRoleFilter to UserRoleFilter.
Filter checks for general role membership instead of only those who have the selected role as top role. Noticed during development that we'd not be able to filter to show all Helpers otherwise, as some Helpers have different top roles such as Core Dev that wouldn't give immediately obvious behaviour to user expectations.
---
pydis_site/apps/api/admin.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 6267b7a8..733a056d 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -380,7 +380,7 @@ class RoleAdmin(admin.ModelAdmin):
return False
-class UserTopRoleFilter(admin.SimpleListFilter):
+class UserRoleFilter(admin.SimpleListFilter):
"""List Filter for User list Admin page."""
title = "Role"
@@ -426,7 +426,7 @@ class UserAdmin(admin.ModelAdmin):
all_roles_coloured.short_description = "All Roles"
search_fields = ("name", "id", "roles")
- list_filter = (UserTopRoleFilter, "in_guild")
+ list_filter = (UserRoleFilter, "in_guild")
list_display = ("username", "top_role_coloured", "in_guild")
fields = ("username", "id", "in_guild", "all_roles_coloured")
sortable_by = ("username",)
--
cgit v1.2.3
From 715dd46aa68358cdd2abee07b018ee08e54da8d9 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Sep 2020 05:02:48 +1000
Subject: Allow Nomination end_reason to have a blank value for validation.
Without `blank=True`, admin page editable forms could not be saved if no content was in the end_reason input.
---
pydis_site/apps/api/models/bot/nomination.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index 54f56c98..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,
--
cgit v1.2.3
From d39eeb9b7e936f264abcfbb998453179ef8556f5 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Sep 2020 06:27:16 +1000
Subject: Change Infraction admin to use fieldsets for better grouping of info.
---
pydis_site/apps/api/admin.py | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 733a056d..c3f1179e 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -72,15 +72,11 @@ class InfractionActorFilter(admin.SimpleListFilter):
class InfractionAdmin(admin.ModelAdmin):
"""Admin formatting for the Infraction model."""
- fields = (
- "user",
- "actor",
- "type",
- "reason",
- "inserted_at",
- "expires_at",
- "active",
- "hidden"
+ fieldsets = (
+ ("Members", {"fields": ("user", "actor")}),
+ ("Action", {"fields": ("type", "hidden", "active")}),
+ ("Dates", {"fields": ("inserted_at", "expires_at")}),
+ ("Reason", {"fields": ("reason",)}),
)
readonly_fields = (
"user",
--
cgit v1.2.3
From 9344d8ac63df05d67a79fbeacc7a8c31acf86853 Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Sep 2020 06:35:38 +1000
Subject: Change documentation link model to order by package.
---
pydis_site/apps/api/models/bot/documentation_link.py | 5 +++++
1 file changed, 5 insertions(+)
(limited to 'pydis_site')
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']
--
cgit v1.2.3
From 1916074a704fc6f4b8595b8afb5463436b60961a Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Sep 2020 06:36:30 +1000
Subject: Add migrations for nomination and doc link model changes.
---
.../apps/api/migrations/0064_auto_20200919_1900.py | 76 ++++++++++++++++++++++
.../apps/api/migrations/0065_auto_20200919_2033.py | 17 +++++
2 files changed, 93 insertions(+)
create mode 100644 pydis_site/apps/api/migrations/0064_auto_20200919_1900.py
create mode 100644 pydis_site/apps/api/migrations/0065_auto_20200919_2033.py
(limited to 'pydis_site')
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']},
+ ),
+ ]
--
cgit v1.2.3
From a105de702d2f64a66acfc2f06a3b994cd46a5c3e Mon Sep 17 00:00:00 2001
From: scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Sep 2020 07:06:40 +1000
Subject: Remove delete permission for bot settings admin.
I'm unable to see any cases where this would be wanted, and instead accidental deletion would result in the system possibly breaking, as we are unable to add the setting again to replace it if it got removed.
The name has also set to read only in item view, to prevent renames, effectively doing the same thing as deleting it.
---
pydis_site/apps/api/admin.py | 5 +++++
1 file changed, 5 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index c3f1179e..5093e605 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -33,11 +33,16 @@ class BotSettingAdmin(admin.ModelAdmin):
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
+
@admin.register(DocumentationLink)
class DocumentationLinkAdmin(admin.ModelAdmin):
--
cgit v1.2.3
From 48fcd02fc53f5ab10a4c03458a2bc0db778b54ea Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Tue, 22 Sep 2020 02:11:22 +0530
Subject: optimize bulk update endpoint by using Model.objects.bulk_update()
method
The Model.objects.bulk_update() method greatly reduces the number of SQL queries by updating all required instances in 1 query.
---
pydis_site/apps/api/serializers.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index ae57b307..1f24d29f 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -281,10 +281,16 @@ class UserListSerializer(ListSerializer):
data_mapping = {item['id']: item for item in validated_data}
updated = []
+ fields_to_update = set()
for user_id, data in data_mapping.items():
+ for key in data:
+ fields_to_update.add(key)
user = instance_mapping.get(user_id)
- updated.append(self.child.update(user, data))
+ user.__dict__.update(data)
+ updated.append(user)
+ fields_to_update.remove("id")
+ User.objects.bulk_update(updated, fields_to_update)
return updated
--
cgit v1.2.3
From 9a98122a2544fc943b05015b156769ce2a53d0b6 Mon Sep 17 00:00:00 2001
From: Den4200
Date: Sat, 26 Sep 2020 20:13:06 -0400
Subject: Add "Welcome to Python Discord" video to index
---
pydis_site/templates/home/index.html | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html
index 3e96cc91..f31363a4 100644
--- a/pydis_site/templates/home/index.html
+++ b/pydis_site/templates/home/index.html
@@ -39,9 +39,7 @@
{# Right column container #}
--
cgit v1.2.3
From 7a12a372cc0202a208e794b36774b9e56d2be096 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Thu, 1 Oct 2020 19:45:44 +0100
Subject: Update rule 5
---
pydis_site/apps/api/views.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py
index 7ac56641..0d126051 100644
--- a/pydis_site/apps/api/views.py
+++ b/pydis_site/apps/api/views.py
@@ -135,8 +135,9 @@ class RulesView(APIView):
),
(
"Do not provide or request help on projects that may break laws, "
- "breach terms of services, be considered malicious/inappropriate "
- "or be for graded coursework/exams."
+ "breach terms of services, be considered malicious or inappropriate. "
+ "Do not help with ongoing exams. Do not provide or request solutions "
+ "for graded assignments, although general guidance is okay."
),
(
"No spamming or unapproved advertising, including requests for paid work. "
--
cgit v1.2.3
From 3e4661e984d85d4df6c2e12655bbde078efbaeb8 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Fri, 2 Oct 2020 23:45:27 +0100
Subject: Only request the identify scope with Discord on OAuth2 login
---
pydis_site/settings.py | 7 +++++++
1 file changed, 7 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 1f042c1b..eee559e4 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -401,3 +401,10 @@ ACCOUNT_USERNAME_VALIDATORS = "pydis_site.VALIDATORS"
LOGIN_REDIRECT_URL = "home"
SOCIALACCOUNT_ADAPTER = "pydis_site.utils.account.SocialAccountAdapter"
+SOCIALACCOUNT_PROVIDERS = {
+ 'discord': {
+ 'SCOPE': [
+ 'identify',
+ ],
+ }
+}
--
cgit v1.2.3
From 4b07fe054215d9cfd5a66214761c74608f6448d5 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Sat, 3 Oct 2020 00:04:28 +0100
Subject: Display no prompt to users who have already authenticated
---
pydis_site/settings.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index eee559e4..3769fa25 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -402,9 +402,10 @@ ACCOUNT_USERNAME_VALIDATORS = "pydis_site.VALIDATORS"
LOGIN_REDIRECT_URL = "home"
SOCIALACCOUNT_ADAPTER = "pydis_site.utils.account.SocialAccountAdapter"
SOCIALACCOUNT_PROVIDERS = {
- 'discord': {
- 'SCOPE': [
- 'identify',
+ "discord": {
+ "SCOPE": [
+ "identify",
],
+ "AUTH_PARAMS": {"prompt": "none"}
}
}
--
cgit v1.2.3
From b1fe351399f3ec70d82cb312d5f791aa27fcdbdf Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 09:40:53 +0300
Subject: Logs Cleanup: Remove logs admin
---
pydis_site/apps/api/admin.py | 28 ----------------------------
1 file changed, 28 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index 5093e605..b6fee9d1 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -14,7 +14,6 @@ from .models import (
DeletedMessage,
DocumentationLink,
Infraction,
- LogEntry,
MessageDeletionContext,
Nomination,
OffTopicChannelName,
@@ -120,33 +119,6 @@ class InfractionAdmin(admin.ModelAdmin):
return False
-@admin.register(LogEntry)
-class LogEntryAdmin(admin.ModelAdmin):
- """Allows viewing logs in the Django Admin without allowing edits."""
-
- actions = None
- list_display = ('timestamp', 'level', 'message')
- fieldsets = (
- ('Overview', {'fields': ('timestamp', 'application', 'logger_name')}),
- ('Metadata', {'fields': ('level', 'module', 'line')}),
- ('Contents', {'fields': ('message',)})
- )
- list_filter = ('level', 'timestamp')
- search_fields = ('message',)
-
- def has_add_permission(self, request: HttpRequest) -> bool:
- """Deny manual LogEntry creation."""
- return False
-
- 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.register(DeletedMessage)
class DeletedMessageAdmin(admin.ModelAdmin):
"""Admin formatting for the DeletedMessage model."""
--
cgit v1.2.3
From e4d1803791ee007086fe9e015e1d57745addeeb4 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 09:42:29 +0300
Subject: Logs Cleanup: Remove logs viewset
---
pydis_site/apps/api/viewsets/__init__.py | 1 -
pydis_site/apps/api/viewsets/log_entry.py | 36 -------------------------------
2 files changed, 37 deletions(-)
delete mode 100644 pydis_site/apps/api/viewsets/log_entry.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py
index dfbb880d..f133e77f 100644
--- a/pydis_site/apps/api/viewsets/__init__.py
+++ b/pydis_site/apps/api/viewsets/__init__.py
@@ -12,4 +12,3 @@ from .bot import (
RoleViewSet,
UserViewSet
)
-from .log_entry import LogEntryViewSet
diff --git a/pydis_site/apps/api/viewsets/log_entry.py b/pydis_site/apps/api/viewsets/log_entry.py
deleted file mode 100644
index 9108a4fa..00000000
--- a/pydis_site/apps/api/viewsets/log_entry.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from rest_framework.mixins import CreateModelMixin
-from rest_framework.viewsets import GenericViewSet
-
-from pydis_site.apps.api.models.log_entry import LogEntry
-from pydis_site.apps.api.serializers import LogEntrySerializer
-
-
-class LogEntryViewSet(CreateModelMixin, GenericViewSet):
- """
- View supporting the creation of log entries in the database for viewing via the log browser.
-
- ## Routes
- ### POST /logs
- Create a new log entry.
-
- #### Request body
- >>> {
- ... 'application': str, # 'bot' | 'seasonalbot' | 'site'
- ... 'logger_name': str, # such as 'bot.cogs.moderation'
- ... 'timestamp': Optional[str], # from `datetime.utcnow().isoformat()`
- ... 'level': str, # 'debug' | 'info' | 'warning' | 'error' | 'critical'
- ... 'module': str, # such as 'pydis_site.apps.api.serializers'
- ... 'line': int, # > 0
- ... 'message': str, # textual formatted content of the logline
- ... }
-
- #### Status codes
- - 201: returned on success
- - 400: if the request body has invalid fields, see the response for details
-
- ## Authentication
- Requires a API token.
- """
-
- queryset = LogEntry.objects.all()
- serializer_class = LogEntrySerializer
--
cgit v1.2.3
From f0d4982b48fe6c6998564d6ca3f10a3a5fc16d77 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 09:46:01 +0300
Subject: Logs Cleanup: Remove logs URL
---
pydis_site/apps/api/urls.py | 2 --
1 file changed, 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py
index 4dbf93db..2e1ef0b4 100644
--- a/pydis_site/apps/api/urls.py
+++ b/pydis_site/apps/api/urls.py
@@ -8,7 +8,6 @@ from .viewsets import (
DocumentationLinkViewSet,
FilterListViewSet,
InfractionViewSet,
- LogEntryViewSet,
NominationViewSet,
OffTopicChannelNameViewSet,
OffensiveMessageViewSet,
@@ -71,7 +70,6 @@ urlpatterns = (
#
# from django_hosts.resolvers import reverse
path('bot/', include((bot_router.urls, 'api'), namespace='bot')),
- path('logs', LogEntryViewSet.as_view({'post': 'create'}), name='logs'),
path('healthcheck', HealthcheckView.as_view(), name='healthcheck'),
path('rules', RulesView.as_view(), name='rules')
)
--
cgit v1.2.3
From d5f20aad49c24ab32af4cb61fb8fc8daf90018bc Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 09:47:59 +0300
Subject: Logs Cleanup: Remove logs serializers
---
pydis_site/apps/api/serializers.py | 13 -------------
1 file changed, 13 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 90bd6f91..86c4cc4b 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -191,19 +191,6 @@ class ExpandedInfractionSerializer(InfractionSerializer):
return ret
-class LogEntrySerializer(ModelSerializer):
- """A class providing (de-)serialization of `LogEntry` instances."""
-
- class Meta:
- """Metadata defined for the Django REST Framework."""
-
- model = LogEntry
- fields = (
- 'application', 'logger_name', 'timestamp',
- 'level', 'module', 'line', 'message'
- )
-
-
class OffTopicChannelNameSerializer(ModelSerializer):
"""A class providing (de-)serialization of `OffTopicChannelName` instances."""
--
cgit v1.2.3
From 33d3d1abce8f6e41772021fb1be5d459d5717046 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 09:50:17 +0300
Subject: Logs Cleanup: Remove site internal logging to DB
---
pydis_site/apps/api/dblogger.py | 22 ----------------------
pydis_site/apps/api/tests/test_dblogger.py | 27 ---------------------------
pydis_site/settings.py | 3 ---
3 files changed, 52 deletions(-)
delete mode 100644 pydis_site/apps/api/dblogger.py
delete mode 100644 pydis_site/apps/api/tests/test_dblogger.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/dblogger.py b/pydis_site/apps/api/dblogger.py
deleted file mode 100644
index 4b4e3a9d..00000000
--- a/pydis_site/apps/api/dblogger.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from logging import LogRecord, StreamHandler
-
-
-class DatabaseLogHandler(StreamHandler):
- """Logs entries into the database."""
-
- def emit(self, record: LogRecord) -> None:
- """Write the given `record` into the database."""
- # This import needs to be deferred due to Django's application
- # registry instantiation logic loading this handler before the
- # application is ready.
- from pydis_site.apps.api.models.log_entry import LogEntry
-
- entry = LogEntry(
- application='site',
- logger_name=record.name,
- level=record.levelname.lower(),
- module=record.module,
- line=record.lineno,
- message=self.format(record)
- )
- entry.save()
diff --git a/pydis_site/apps/api/tests/test_dblogger.py b/pydis_site/apps/api/tests/test_dblogger.py
deleted file mode 100644
index bb19f297..00000000
--- a/pydis_site/apps/api/tests/test_dblogger.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import logging
-from datetime import datetime
-
-from django.test import TestCase
-
-from ..dblogger import DatabaseLogHandler
-from ..models import LogEntry
-
-
-class DatabaseLogHandlerTests(TestCase):
- def test_logs_to_database(self):
- module_basename = __name__.split('.')[-1]
- logger = logging.getLogger(__name__)
- logger.handlers = [DatabaseLogHandler()]
- logger.warning("I am a test case!")
-
- # Ensure we only have a single record in the database
- # after the logging call above.
- [entry] = LogEntry.objects.all()
-
- self.assertEqual(entry.application, 'site')
- self.assertEqual(entry.logger_name, __name__)
- self.assertIsInstance(entry.timestamp, datetime)
- self.assertEqual(entry.level, 'warning')
- self.assertEqual(entry.module, module_basename)
- self.assertIsInstance(entry.line, int)
- self.assertEqual(entry.message, "I am a test case!")
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 3769fa25..b2e1e7f4 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -260,9 +260,6 @@ LOGGING = {
'handlers': {
'console': {
'class': 'logging.StreamHandler'
- },
- 'database': {
- 'class': 'pydis_site.apps.api.dblogger.DatabaseLogHandler'
}
},
'loggers': {
--
cgit v1.2.3
From 28300128516369de2f235cc7d0427ebceb6aa28d Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 10:09:26 +0300
Subject: Logs Cleanup: Remove another entry of site internal logging to DB
---
pydis_site/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index b2e1e7f4..5eb812ac 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -264,7 +264,7 @@ LOGGING = {
},
'loggers': {
'django': {
- 'handlers': ['console', 'database'],
+ 'handlers': ['console'],
'propagate': True,
'level': env(
'LOG_LEVEL',
--
cgit v1.2.3
From c262b80c24450b7f2b24eb51b994495107e1aac6 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 10:09:46 +0300
Subject: Logs Cleanup: Remove log entry model
---
pydis_site/apps/api/models/__init__.py | 1 -
pydis_site/apps/api/models/log_entry.py | 55 ---------------------------------
2 files changed, 56 deletions(-)
delete mode 100644 pydis_site/apps/api/models/log_entry.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py
index e3f928e1..0a8c90f6 100644
--- a/pydis_site/apps/api/models/__init__.py
+++ b/pydis_site/apps/api/models/__init__.py
@@ -14,4 +14,3 @@ from .bot import (
Role,
User
)
-from .log_entry import LogEntry
diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py
deleted file mode 100644
index 752cd2ca..00000000
--- a/pydis_site/apps/api/models/log_entry.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from django.db import models
-from django.utils import timezone
-
-from pydis_site.apps.api.models.mixins import ModelReprMixin
-
-
-class LogEntry(ModelReprMixin, models.Model):
- """A log entry generated by one of the PyDis applications."""
-
- application = models.CharField(
- max_length=20,
- help_text="The application that generated this log entry.",
- choices=(
- ('bot', 'Bot'),
- ('seasonalbot', 'Seasonalbot'),
- ('site', 'Website')
- )
- )
- logger_name = models.CharField(
- max_length=100,
- help_text="The name of the logger that generated this log entry."
- )
- timestamp = models.DateTimeField(
- default=timezone.now,
- help_text="The date and time when this entry was created."
- )
- level = models.CharField(
- max_length=8, # 'critical'
- choices=(
- ('debug', 'Debug'),
- ('info', 'Info'),
- ('warning', 'Warning'),
- ('error', 'Error'),
- ('critical', 'Critical')
- ),
- help_text=(
- "The logger level at which this entry was emitted. The levels "
- "correspond to the Python `logging` levels."
- )
- )
- module = models.CharField(
- max_length=100,
- help_text="The fully qualified path of the module generating this log line."
- )
- line = models.PositiveSmallIntegerField(
- help_text="The line at which the log line was emitted."
- )
- message = models.TextField(
- help_text="The textual content of the log line."
- )
-
- class Meta:
- """Customizes the default generated plural name to valid English."""
-
- verbose_name_plural = 'Log entries'
--
cgit v1.2.3
From e40fc796bf867143e2c73a4f7b6bf90a1b63fe76 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 10:10:12 +0300
Subject: Logs Cleanup: Add migration for log entry removal
---
pydis_site/apps/api/migrations/0064_delete_logentry.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
create mode 100644 pydis_site/apps/api/migrations/0064_delete_logentry.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0064_delete_logentry.py b/pydis_site/apps/api/migrations/0064_delete_logentry.py
new file mode 100644
index 00000000..a5f344d1
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0064_delete_logentry.py
@@ -0,0 +1,16 @@
+# Generated by Django 3.0.9 on 2020-10-03 06:57
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0063_Allow_blank_or_null_for_nomination_reason'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='LogEntry',
+ ),
+ ]
--
cgit v1.2.3
From be671afdfc296e67e74791ded1674d2ce00af364 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 10:19:32 +0300
Subject: Logs Cleanup: Remove removed LogEntry model import
---
pydis_site/apps/api/serializers.py | 1 -
1 file changed, 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 86c4cc4b..f9a5517e 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -9,7 +9,6 @@ from .models import (
DocumentationLink,
FilterList,
Infraction,
- LogEntry,
MessageDeletionContext,
Nomination,
OffTopicChannelName,
--
cgit v1.2.3
From 9aca77b6853cd106d265404f017d4697d04d1b25 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 3 Oct 2020 10:31:04 +0300
Subject: Logs Cleanup: Fix migrations conflicts with merge migration
---
pydis_site/apps/api/migrations/0066_merge_20201003_0730.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 pydis_site/apps/api/migrations/0066_merge_20201003_0730.py
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py b/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py
new file mode 100644
index 00000000..298416db
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0066_merge_20201003_0730.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.9 on 2020-10-03 07:30
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0064_delete_logentry'),
+ ('api', '0065_auto_20200919_2033'),
+ ]
+
+ operations = [
+ ]
--
cgit v1.2.3
From d8de1be9906a19b79b172b61f4b96189497c6ee1 Mon Sep 17 00:00:00 2001
From: imSofi <20756843+imsofi@users.noreply.github.com>
Date: Mon, 5 Oct 2020 02:58:19 +0200
Subject: Bump links for Effective Python to 2nd edition
Updated and expanded book for Python 3. Has 30 new major guidelines added compared to the 1st edition.
---
pydis_site/apps/home/resources/books/effective_python.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/home/resources/books/effective_python.yaml b/pydis_site/apps/home/resources/books/effective_python.yaml
index ab782704..7f9d0dea 100644
--- a/pydis_site/apps/home/resources/books/effective_python.yaml
+++ b/pydis_site/apps/home/resources/books/effective_python.yaml
@@ -1,4 +1,4 @@
-description: A book that gives 59 best practices for writing excellent Python. Great
+description: A book that gives 90 best practices for writing excellent Python. Great
for intermediates.
name: Effective Python
payment: paid
@@ -8,7 +8,7 @@ urls:
url: https://effectivepython.com/
- icon: branding/amazon
title: Amazon
- url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134034287
+ url: https://www.amazon.com/Effective-Python-Specific-Software-Development/dp/0134853989
- icon: branding/github
title: GitHub
url: https://github.com/bslatkin/effectivepython
--
cgit v1.2.3
From e7a61d0a2c96f48c7495f1f25e56384f6dbac645 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Mon, 5 Oct 2020 18:18:09 +0530
Subject: raise validation error for user not found in UserListSerializer
---
pydis_site/apps/api/serializers.py | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 1f24d29f..a560d491 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -278,16 +278,20 @@ class UserListSerializer(ListSerializer):
ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update
"""
instance_mapping = {user.id: user for user in instance}
- data_mapping = {item['id']: item for item in validated_data}
updated = []
fields_to_update = set()
- for user_id, data in data_mapping.items():
- for key in data:
+ for user_data in validated_data:
+ for key in user_data:
fields_to_update.add(key)
- user = instance_mapping.get(user_id)
- user.__dict__.update(data)
- updated.append(user)
+
+ try:
+ user = instance_mapping[user_data["id"]]
+ except KeyError:
+ raise ValidationError({"id": f"User with id {user_data['id']} not found."})
+
+ user.__dict__.update(user_data)
+ updated.append(user)
fields_to_update.remove("id")
User.objects.bulk_update(updated, fields_to_update)
@@ -297,6 +301,7 @@ class UserListSerializer(ListSerializer):
class UserSerializer(ModelSerializer):
"""A class providing (de-)serialization of `User` instances."""
+ # ID field must be explicitly set as the default id field is read-only.
id = IntegerField(min_value=0)
class Meta:
--
cgit v1.2.3
From a95603405a1f7d80845fdf8e4719ce7f4c385594 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Mon, 5 Oct 2020 18:19:45 +0530
Subject: return next and previous page numbers in paginator instead of links
---
pydis_site/apps/api/viewsets/bot/user.py | 41 +++++++++++++++++++++++---------
1 file changed, 30 insertions(+), 11 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index d015fe71..0dd529be 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,3 +1,5 @@
+from collections import OrderedDict
+
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
@@ -16,6 +18,30 @@ class UserListPagination(PageNumberPagination):
page_size = 10000
page_size_query_param = "page_size"
+ def get_next_page_number(self) -> int:
+ """Get the next page number."""
+ if not self.page.has_next():
+ return None
+ page_number = self.page.next_page_number()
+ return page_number
+
+ def get_previous_page_number(self) -> int:
+ """Get the previous page number."""
+ if not self.page.has_previous():
+ return None
+
+ page_number = self.page.previous_page_number()
+ return page_number
+
+ def get_paginated_response(self, data: list) -> Response:
+ """Override method to send modified response."""
+ return Response(OrderedDict([
+ ('count', self.page.paginator.count),
+ ('next_page_no', self.get_next_page_number()),
+ ('previous_page_no', self.get_previous_page_number()),
+ ('results', data)
+ ]))
+
class UserViewSet(ModelViewSet):
"""
@@ -193,13 +219,6 @@ class UserViewSet(ModelViewSet):
filtered_instances = queryset.filter(id__in=object_ids)
- if filtered_instances.count() != len(object_ids):
- # If all user objects passed in request.body are not present in the database.
- resp = {
- "Error": "User object not found."
- }
- return Response(resp, status=status.HTTP_404_NOT_FOUND)
-
serializer = self.get_serializer(
instance=filtered_instances,
data=request.data,
@@ -207,7 +226,7 @@ class UserViewSet(ModelViewSet):
partial=True
)
- if serializer.is_valid():
- serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
--
cgit v1.2.3
From 69342e8cc952ff2dd25ac50f7a80d30e6d409a0f Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Mon, 5 Oct 2020 18:20:43 +0530
Subject: Change test case to use updated code
---
pydis_site/apps/api/tests/test_users.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 1f9bd687..affc2c48 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -213,7 +213,7 @@ class MultiPatchTests(APISubdomainTestCase):
}
]
response = self.client.patch(url, data=data)
- self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.status_code, 400)
def test_returns_400_for_bad_data(self):
url = reverse("bot:user-bulk-patch", host="api")
--
cgit v1.2.3
From 3eee5e76894995cea87391d60c9749eb66b3187a Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Tue, 6 Oct 2020 21:21:53 +0530
Subject: Update docstring of UserViewSet and correct function annotation on
UserListPagination
---
pydis_site/apps/api/viewsets/bot/user.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 0dd529be..ebea6a45 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,3 +1,4 @@
+import typing
from collections import OrderedDict
from rest_framework import status
@@ -18,14 +19,14 @@ class UserListPagination(PageNumberPagination):
page_size = 10000
page_size_query_param = "page_size"
- def get_next_page_number(self) -> int:
+ def get_next_page_number(self) -> typing.Optional[int]:
"""Get the next page number."""
if not self.page.has_next():
return None
page_number = self.page.next_page_number()
return page_number
- def get_previous_page_number(self) -> int:
+ def get_previous_page_number(self) -> typing.Optional[int]:
"""Get the previous page number."""
if not self.page.has_previous():
return None
@@ -54,8 +55,8 @@ class UserViewSet(ModelViewSet):
#### Response format
>>> {
... 'count': 95000,
- ... 'next': "http://api.pythondiscord.com/bot/users?page=2",
- ... 'previous': None,
+ ... 'next_page_no': "2",
+ ... 'previous_page_no': None,
... 'results': [
... {
... 'id': 409107086526644234,
--
cgit v1.2.3
From 6b28ac6eec610871753d63fa95ccc7311c759181 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Tue, 6 Oct 2020 21:24:50 +0530
Subject: overhaul create method to ignore conflicts and raise error when
duplicates present in request data.
Raising NotFound exception instead of ValidatoinError in update method
---
pydis_site/apps/api/serializers.py | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index a560d491..51afbe73 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -1,5 +1,6 @@
"""Converters from Django models to data interchange formats and back."""
from django.db.models.query import QuerySet
+from rest_framework.exceptions import NotFound
from rest_framework.serializers import (
IntegerField,
ListSerializer,
@@ -260,16 +261,16 @@ class UserListSerializer(ListSerializer):
def create(self, validated_data: list) -> list:
"""Override create method to optimize django queries."""
- present_users = User.objects.all()
new_users = []
- present_user_ids = [user.id for user in present_users]
+ request_user_ids = [user["id"] for user in validated_data]
for user_dict in validated_data:
- if user_dict["id"] in present_user_ids:
- raise ValidationError({"id": "User already exists."})
+ if request_user_ids.count(user_dict["id"]) > 1:
+ raise ValidationError({"id": f"User with ID {user_dict['id']} "
+ f"given multiple times."})
new_users.append(User(**user_dict))
- return User.objects.bulk_create(new_users)
+ return User.objects.bulk_create(new_users, ignore_conflicts=True)
def update(self, instance: QuerySet, validated_data: list) -> list:
"""
@@ -288,7 +289,7 @@ class UserListSerializer(ListSerializer):
try:
user = instance_mapping[user_data["id"]]
except KeyError:
- raise ValidationError({"id": f"User with id {user_data['id']} not found."})
+ raise NotFound({"id": f"User with id {user_data['id']} not found."})
user.__dict__.update(user_data)
updated.append(user)
--
cgit v1.2.3
From 3c63323deea17a98a5c4ad899d868c9171edbacb Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Tue, 6 Oct 2020 21:26:04 +0530
Subject: refactor tests to use updated changes and add tests for
UserListPagination
---
pydis_site/apps/api/tests/test_users.py | 66 ++++++++++++++++++++++++++++++++-
1 file changed, 64 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index affc2c48..fb22c627 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -123,7 +123,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 400)
def test_returns_400_for_user_recreation(self):
- """Return 400 if User is already present in database."""
+ """Return 201 if User is already present in database as it skips User creation."""
url = reverse('bot:user-list', host='api')
data = [{
'id': 11,
@@ -132,6 +132,26 @@ class CreationTests(APISubdomainTestCase):
'in_guild': True
}]
response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 201)
+
+ def test_returns_400_for_duplicate_request_users(self):
+ """Return 400 if 2 Users with same ID is passed in the request data."""
+ url = reverse('bot:user-list', host='api')
+ data = [
+ {
+ 'id': 11,
+ 'name': 'You saw nothing.',
+ 'discriminator': 112,
+ 'in_guild': True
+ },
+ {
+ 'id': 11,
+ 'name': 'You saw nothing part 2.',
+ 'discriminator': 1122,
+ 'in_guild': False
+ }
+ ]
+ response = self.client.post(url, data=data)
self.assertEqual(response.status_code, 400)
@@ -213,7 +233,7 @@ class MultiPatchTests(APISubdomainTestCase):
}
]
response = self.client.patch(url, data=data)
- self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.status_code, 404)
def test_returns_400_for_bad_data(self):
url = reverse("bot:user-bulk-patch", host="api")
@@ -286,3 +306,45 @@ class UserModelTests(APISubdomainTestCase):
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")
+
+
+class UserPaginatorTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ users = []
+ for i in range(1, 10_001):
+ users.append(User(
+ id=i,
+ name=f"user{i}",
+ discriminator=1111,
+ in_guild=True
+ ))
+ cls.users = User.objects.bulk_create(users)
+
+ def test_returns_single_page_response(self):
+ url = reverse("bot:user-list", host="api")
+ response = self.client.get(url).json()
+ self.assertIsNone(response["next_page_no"])
+ self.assertIsNone(response["previous_page_no"])
+
+ def test_returns_next_page_number(self):
+ User.objects.create(
+ id=10_001,
+ name="user10001",
+ discriminator=1111,
+ in_guild=True
+ )
+ url = reverse("bot:user-list", host="api")
+ response = self.client.get(url).json()
+ self.assertEqual(2, response["next_page_no"])
+
+ def test_returns_previous_page_number(self):
+ User.objects.create(
+ id=10_001,
+ name="user10001",
+ discriminator=1111,
+ in_guild=True
+ )
+ url = reverse("bot:user-list", host="api")
+ response = self.client.get(url, {"page": 2}).json()
+ self.assertEqual(1, response["previous_page_no"])
--
cgit v1.2.3
From 7b2275dd0888cc4e91866921ab3be5799eb44986 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Wed, 7 Oct 2020 09:38:45 +0530
Subject: use more efficient code
---
pydis_site/apps/api/serializers.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 51afbe73..ed6717e7 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -262,12 +262,14 @@ class UserListSerializer(ListSerializer):
def create(self, validated_data: list) -> list:
"""Override create method to optimize django queries."""
new_users = []
- request_user_ids = [user["id"] for user in validated_data]
+ seen = set()
for user_dict in validated_data:
- if request_user_ids.count(user_dict["id"]) > 1:
- raise ValidationError({"id": f"User with ID {user_dict['id']} "
- f"given multiple times."})
+ if user_dict["id"] in seen:
+ raise ValidationError(
+ {"id": f"User with ID {user_dict['id']} given multiple times."}
+ )
+ seen.add(user_dict["id"])
new_users.append(User(**user_dict))
return User.objects.bulk_create(new_users, ignore_conflicts=True)
--
cgit v1.2.3
From 5d3d305648acce800769c620760b3a642b399276 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Wed, 7 Oct 2020 09:42:28 +0530
Subject: Document changes made to UserListSerializer in UserViewSet
---
pydis_site/apps/api/viewsets/bot/user.py | 3 +++
1 file changed, 3 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index ebea6a45..54723890 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -104,6 +104,8 @@ class UserViewSet(ModelViewSet):
### POST /bot/users
Adds a single or multiple new users.
The roles attached to the user(s) must be roles known by the site.
+ User creation process will be skipped if user is already present in the database.
+ multiple users with the same id in the request data will raise an error.
#### Request body
>>> {
@@ -120,6 +122,7 @@ class UserViewSet(ModelViewSet):
#### Status codes
- 201: returned on success
- 400: if one of the given roles does not exist, or one of the given fields is invalid
+ - 400: if multiple user objects with the same id is given.
### PUT /bot/users/
Update the user with the given `snowflake`.
--
cgit v1.2.3
From 6201c91895b9e7171d63618a74764cfdda4d4fe0 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Wed, 7 Oct 2020 09:55:21 +0530
Subject: add check for insufficient data in update method of
UserListSerializer
---
pydis_site/apps/api/serializers.py | 5 +++++
1 file changed, 5 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index ed6717e7..98d58e97 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -297,6 +297,11 @@ class UserListSerializer(ListSerializer):
updated.append(user)
fields_to_update.remove("id")
+
+ if not fields_to_update:
+ # Raise ValidationError when only id field is given.
+ raise ValidationError({"data": "Insufficient data provided."})
+
User.objects.bulk_update(updated, fields_to_update)
return updated
--
cgit v1.2.3
From a3bd6568770b54da21147d386f3cadbdd265f69e Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Wed, 7 Oct 2020 09:56:31 +0530
Subject: add test case for insufficient data while bulk updating users.
---
pydis_site/apps/api/tests/test_users.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index fb22c627..52f5d213 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -251,6 +251,20 @@ class MultiPatchTests(APISubdomainTestCase):
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
+ def test_returns_400_for_insufficient_data(self):
+ url = reverse("bot:user-bulk-patch", host="api")
+ data = [
+ {
+ "id": 1,
+
+ },
+ {
+ "id": 2,
+ }
+ ]
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+
class UserModelTests(APISubdomainTestCase):
@classmethod
--
cgit v1.2.3
From 5388034d9a00e41f18ba5f476a2c3e347d4bd569 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Wed, 7 Oct 2020 11:24:31 +0530
Subject: update documentation
---
pydis_site/apps/api/viewsets/bot/user.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 54723890..740fc439 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -105,7 +105,6 @@ class UserViewSet(ModelViewSet):
Adds a single or multiple new users.
The roles attached to the user(s) must be roles known by the site.
User creation process will be skipped if user is already present in the database.
- multiple users with the same id in the request data will raise an error.
#### Request body
>>> {
@@ -122,7 +121,7 @@ class UserViewSet(ModelViewSet):
#### Status codes
- 201: returned on success
- 400: if one of the given roles does not exist, or one of the given fields is invalid
- - 400: if multiple user objects with the same id is given.
+ - 400: if multiple user objects with the same id are given.
### PUT /bot/users/
Update the user with the given `snowflake`.
@@ -208,8 +207,9 @@ class UserViewSet(ModelViewSet):
... ]
#### Status codes
- - 200: Returned on success.
+ - 200: returned on success.
- 400: if the request body was invalid, see response body for details.
+ - 404: if the user with the given id does not exist.
"""
queryset = self.get_queryset()
try:
--
cgit v1.2.3
From 995c62b509242d5ae75f8c7a19840ff0f1832d44 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 12:24:05 +0530
Subject: catch IntegrityError and raise ValidationError during user creation.
---
pydis_site/apps/api/serializers.py | 8 ++++++++
1 file changed, 8 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 98d58e97..a36bf72f 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -1,5 +1,6 @@
"""Converters from Django models to data interchange formats and back."""
from django.db.models.query import QuerySet
+from django.db.utils import IntegrityError
from rest_framework.exceptions import NotFound
from rest_framework.serializers import (
IntegerField,
@@ -320,6 +321,13 @@ class UserSerializer(ModelSerializer):
depth = 1
list_serializer_class = UserListSerializer
+ def create(self, validated_data: dict) -> User:
+ """Override create method to catch IntegrityError."""
+ try:
+ return super().create(validated_data)
+ except IntegrityError:
+ raise ValidationError({"ID": "User with ID already present."})
+
class NominationSerializer(ModelSerializer):
"""A class providing (de-)serialization of `Nomination` instances."""
--
cgit v1.2.3
From 69693337cfe89df6ae59c50e9076b6d935e24a50 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 12:30:51 +0530
Subject: fix bug: bulk_patch returns duplicate objects in the response
---
pydis_site/apps/api/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index a36bf72f..a4410566 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -295,7 +295,7 @@ class UserListSerializer(ListSerializer):
raise NotFound({"id": f"User with id {user_data['id']} not found."})
user.__dict__.update(user_data)
- updated.append(user)
+ updated.append(user)
fields_to_update.remove("id")
--
cgit v1.2.3
From a9074045245d5c301cabc01c79785a2a3f846682 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 12:57:40 +0530
Subject: add test case: test_returns_400_for_existing_user
---
pydis_site/apps/api/tests/test_users.py | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 52f5d213..1569e4d9 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -154,6 +154,18 @@ class CreationTests(APISubdomainTestCase):
response = self.client.post(url, data=data)
self.assertEqual(response.status_code, 400)
+ def test_returns_400_for_existing_user(self):
+ """Returns 400 if user is already present in DB."""
+ url = reverse('bot:user-list', host='api')
+ data = {
+ 'id': 11,
+ 'name': 'You saw nothing part 3.',
+ 'discriminator': 1122,
+ 'in_guild': True
+ }
+ response = self.client.post(url, data=data)
+ self.assertEqual(response.status_code, 400)
+
class MultiPatchTests(APISubdomainTestCase):
@classmethod
@@ -256,7 +268,6 @@ class MultiPatchTests(APISubdomainTestCase):
data = [
{
"id": 1,
-
},
{
"id": 2,
--
cgit v1.2.3
From cb32322b8bacabecccdf896d0f7db0355177ac72 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 13:50:50 +0530
Subject: raise ValidationError if users have same ID in request data during
bulk patch
---
pydis_site/apps/api/viewsets/bot/user.py | 24 +++++++++++++++---------
1 file changed, 15 insertions(+), 9 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 740fc439..46e682d8 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -6,7 +6,7 @@ from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
from rest_framework.request import Request
from rest_framework.response import Response
-from rest_framework.serializers import ModelSerializer
+from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from pydis_site.apps.api.models.bot.user import User
@@ -212,14 +212,20 @@ class UserViewSet(ModelViewSet):
- 404: if the user with the given id does not exist.
"""
queryset = self.get_queryset()
- try:
- object_ids = [item["id"] for item in request.data]
- except KeyError:
- # user ID not provided in request body.
- resp = {
- "Error": "User ID not provided."
- }
- return Response(resp, status=status.HTTP_400_BAD_REQUEST)
+ object_ids = set()
+ for data in request.data:
+ try:
+ if data["id"] in object_ids:
+ # If request data contains users with same ID.
+ raise ValidationError(
+ {"id": [f"User with ID {data['id']} given multiple times."]}
+ )
+ except KeyError:
+ # If user ID not provided in request body.
+ raise ValidationError(
+ {"id": ["This field is required."]}
+ )
+ object_ids.add(data["id"])
filtered_instances = queryset.filter(id__in=object_ids)
--
cgit v1.2.3
From 446db59473706c1c44a9a81a6470a87988a99cfb Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 13:51:44 +0530
Subject: add testcase: test_returns_400_for_duplicate_request_users
---
pydis_site/apps/api/tests/test_users.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 1569e4d9..8ed56e83 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -276,6 +276,22 @@ class MultiPatchTests(APISubdomainTestCase):
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
+ def test_returns_400_for_duplicate_request_users(self):
+ """Return 400 if 2 Users with same ID is passed in the request data."""
+ url = reverse("bot:user-bulk-patch", host="api")
+ data = [
+ {
+ 'id': 1,
+ 'name': 'You saw nothing.',
+ },
+ {
+ 'id': 1,
+ 'name': 'You saw nothing part 2.',
+ }
+ ]
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+
class UserModelTests(APISubdomainTestCase):
@classmethod
--
cgit v1.2.3
From ffab239d674ada91fc4d6c265f6b1aa1c82b9c2c Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 14:12:19 +0530
Subject: update documentation
---
pydis_site/apps/api/viewsets/bot/user.py | 70 ++++++++++++++++----------------
1 file changed, 34 insertions(+), 36 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 46e682d8..77142c30 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -73,9 +73,9 @@ class UserViewSet(ModelViewSet):
... ]
... }
- #### Query Parameters
- - page_size: Number of Users in one page.
- - page: Page number
+ #### Optional Query Parameters
+ - page_size: number of Users in one page, defaults to 10,000
+ - page: page number
#### Status codes
- 200: returned on success
@@ -104,7 +104,7 @@ class UserViewSet(ModelViewSet):
### POST /bot/users
Adds a single or multiple new users.
The roles attached to the user(s) must be roles known by the site.
- User creation process will be skipped if user is already present in the database.
+ Users that already exist in the database will be skipped.
#### Request body
>>> {
@@ -121,7 +121,7 @@ class UserViewSet(ModelViewSet):
#### Status codes
- 201: returned on success
- 400: if one of the given roles does not exist, or one of the given fields is invalid
- - 400: if multiple user objects with the same id are given.
+ - 400: if multiple user objects with the same id are given
### PUT /bot/users/
Update the user with the given `snowflake`.
@@ -159,6 +159,34 @@ class UserViewSet(ModelViewSet):
- 400: if the request body was invalid, see response body for details
- 404: if the user with the given `snowflake` could not be found
+ ### BULK PATCH /bot/users/bulk_patch
+ Update users with the given `ids` and `details`.
+ `id` field and at least one other field is mandatory.
+
+ #### Request body
+ >>> [
+ ... {
+ ... 'id': int,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
+ ... },
+ ... {
+ ... 'id': int,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
+ ... },
+ ... ]
+
+ #### Status codes
+ - 200: returned on success
+ - 400: if the request body was invalid, see response body for details
+ - 400: if multiple user objects with the same id are given
+ - 404: if the user with the given id does not exist
+
### DELETE /bot/users/
Deletes the user with the given `snowflake`.
@@ -180,37 +208,7 @@ class UserViewSet(ModelViewSet):
@action(detail=False, methods=["PATCH"], name='user-bulk-patch')
def bulk_patch(self, request: Request) -> Response:
- """
- Update multiple User objects in a single request.
-
- ## Route
- ### PATCH /bot/users/bulk_patch
- Update all users with the IDs.
- `id` field is mandatory, rest are optional.
-
- #### Request body
- >>> [
- ... {
- ... 'id': int,
- ... 'name': str,
- ... 'discriminator': int,
- ... 'roles': List[int],
- ... 'in_guild': bool
- ... },
- ... {
- ... 'id': int,
- ... 'name': str,
- ... 'discriminator': int,
- ... 'roles': List[int],
- ... 'in_guild': bool
- ... },
- ... ]
-
- #### Status codes
- - 200: returned on success.
- - 400: if the request body was invalid, see response body for details.
- - 404: if the user with the given id does not exist.
- """
+ """Update multiple User objects in a single request."""
queryset = self.get_queryset()
object_ids = set()
for data in request.data:
--
cgit v1.2.3
From fe851bd091a32196b4171b289fc6dba5c2bf65a8 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 14:26:05 +0530
Subject: use NON_FIELD_ERRORS_KEY for non-field-specific ValidationError
response
---
pydis_site/apps/api/serializers.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index a4410566..74d1ac8c 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -9,6 +9,7 @@ from rest_framework.serializers import (
PrimaryKeyRelatedField,
ValidationError
)
+from rest_framework.settings import api_settings
from rest_framework.validators import UniqueTogetherValidator
from .models import (
@@ -301,7 +302,9 @@ class UserListSerializer(ListSerializer):
if not fields_to_update:
# Raise ValidationError when only id field is given.
- raise ValidationError({"data": "Insufficient data provided."})
+ raise ValidationError(
+ {api_settings.NON_FIELD_ERRORS_KEY: ["Insufficient data provided."]}
+ )
User.objects.bulk_update(updated, fields_to_update)
return updated
--
cgit v1.2.3
From cd72f72373a0b317cee6c99cf5f05ad8dfeb77d1 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 14:31:50 +0530
Subject: normalize API error responses.
---
pydis_site/apps/api/serializers.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 74d1ac8c..155f33f2 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -293,7 +293,7 @@ class UserListSerializer(ListSerializer):
try:
user = instance_mapping[user_data["id"]]
except KeyError:
- raise NotFound({"id": f"User with id {user_data['id']} not found."})
+ raise NotFound({"detail": [f"User with id {user_data['id']} not found."]})
user.__dict__.update(user_data)
updated.append(user)
@@ -329,7 +329,7 @@ class UserSerializer(ModelSerializer):
try:
return super().create(validated_data)
except IntegrityError:
- raise ValidationError({"ID": "User with ID already present."})
+ raise ValidationError({"id": ["User with ID already present."]})
class NominationSerializer(ModelSerializer):
--
cgit v1.2.3
From e7413978556567d86c92ff643b6d2173d09f44b7 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 14:32:28 +0530
Subject: correct indentation
---
pydis_site/apps/api/viewsets/bot/user.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 77142c30..22e61915 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -166,18 +166,18 @@ class UserViewSet(ModelViewSet):
#### Request body
>>> [
... {
- ... 'id': int,
- ... 'name': str,
- ... 'discriminator': int,
- ... 'roles': List[int],
- ... 'in_guild': bool
+ ... 'id': int,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
... },
... {
- ... 'id': int,
- ... 'name': str,
- ... 'discriminator': int,
- ... 'roles': List[int],
- ... 'in_guild': bool
+ ... 'id': int,
+ ... 'name': str,
+ ... 'discriminator': int,
+ ... 'roles': List[int],
+ ... 'in_guild': bool
... },
... ]
--
cgit v1.2.3
From bdc69a74e27a65fb9b8ce67a878c6a953f6777b8 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 14:57:50 +0530
Subject: fix bug with bulk create: response includes objects for users which
were duplicates
---
pydis_site/apps/api/serializers.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 155f33f2..4f56e52f 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -274,7 +274,8 @@ class UserListSerializer(ListSerializer):
seen.add(user_dict["id"])
new_users.append(User(**user_dict))
- return User.objects.bulk_create(new_users, ignore_conflicts=True)
+ users = User.objects.bulk_create(new_users, ignore_conflicts=True)
+ return User.objects.filter(id__in=[user.id for user in users])
def update(self, instance: QuerySet, validated_data: list) -> list:
"""
--
cgit v1.2.3
From 570f0cf226ebabbd4f41a19ded90f70d0ec96f9a Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 22:49:57 +0530
Subject: return empty list as response for bulk creation of users
---
pydis_site/apps/api/serializers.py | 4 ++--
pydis_site/apps/api/tests/test_users.py | 2 +-
pydis_site/apps/api/viewsets/bot/user.py | 3 ++-
3 files changed, 5 insertions(+), 4 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index efd4f7ce..31293c86 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -260,8 +260,8 @@ class UserListSerializer(ListSerializer):
seen.add(user_dict["id"])
new_users.append(User(**user_dict))
- users = User.objects.bulk_create(new_users, ignore_conflicts=True)
- return User.objects.filter(id__in=[user.id for user in users])
+ User.objects.bulk_create(new_users, ignore_conflicts=True)
+ return []
def update(self, instance: QuerySet, validated_data: list) -> list:
"""
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 8ed56e83..825e4edb 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -96,7 +96,7 @@ class CreationTests(APISubdomainTestCase):
response = self.client.post(url, data=data)
self.assertEqual(response.status_code, 201)
- self.assertEqual(response.json(), data)
+ self.assertEqual(response.json(), [])
def test_returns_400_for_unknown_role_id(self):
url = reverse('bot:user-list', host='api')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 22e61915..5c509e50 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -116,7 +116,8 @@ class UserViewSet(ModelViewSet):
... }
Alternatively, request users can be POSTed as a list of above objects,
- in which case multiple users will be created at once.
+ in which case multiple users will be created at once. In this case,
+ the response is an empty list.
#### Status codes
- 201: returned on success
--
cgit v1.2.3
From e405b60de8f46ad1ef0ec8b2f0ea8cdd12ef2e55 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 23:02:42 +0530
Subject: supress warning: UnorderedObjectListWarning: Pagination may yield
inconsistent results with an unordered object_list: QuerySet.
---
pydis_site/apps/api/viewsets/bot/user.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 5c509e50..9ddd13d4 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -197,7 +197,7 @@ class UserViewSet(ModelViewSet):
"""
serializer_class = UserSerializer
- queryset = User.objects.all()
+ queryset = User.objects.all().order_by("id")
pagination_class = UserListPagination
def get_serializer(self, *args, **kwargs) -> ModelSerializer:
--
cgit v1.2.3
From a57035b893e223c35d73d0994343d3c407b157fb Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 23:22:18 +0530
Subject: Convert error message value from list to string
---
pydis_site/apps/api/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 31293c86..ac155a78 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -280,7 +280,7 @@ class UserListSerializer(ListSerializer):
try:
user = instance_mapping[user_data["id"]]
except KeyError:
- raise NotFound({"detail": [f"User with id {user_data['id']} not found."]})
+ raise NotFound({"detail": f"User with id {user_data['id']} not found."})
user.__dict__.update(user_data)
updated.append(user)
--
cgit v1.2.3
From 4e43549591f3c027f47de45c5a4a524236af3bb4 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Thu, 8 Oct 2020 23:40:41 +0530
Subject: Convert ValidationError response value to list from string
---
pydis_site/apps/api/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index ac155a78..ee950cf4 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -255,7 +255,7 @@ class UserListSerializer(ListSerializer):
for user_dict in validated_data:
if user_dict["id"] in seen:
raise ValidationError(
- {"id": f"User with ID {user_dict['id']} given multiple times."}
+ {"id": [f"User with ID {user_dict['id']} given multiple times."]}
)
seen.add(user_dict["id"])
new_users.append(User(**user_dict))
--
cgit v1.2.3
From 1bef883886f3335a441b496b35df9e25b3ba0e91 Mon Sep 17 00:00:00 2001
From: RohanJnr
Date: Fri, 9 Oct 2020 00:02:24 +0530
Subject: Move Validation checks to serializer from viewset
---
pydis_site/apps/api/serializers.py | 23 +++++++++++++++++++++--
pydis_site/apps/api/viewsets/bot/user.py | 22 ++--------------------
2 files changed, 23 insertions(+), 22 deletions(-)
(limited to 'pydis_site')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index ee950cf4..25c5c82e 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -263,13 +263,32 @@ class UserListSerializer(ListSerializer):
User.objects.bulk_create(new_users, ignore_conflicts=True)
return []
- def update(self, instance: QuerySet, validated_data: list) -> list:
+ def update(self, queryset: QuerySet, validated_data: list) -> list:
"""
Override update method to support bulk updates.
ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update
"""
- instance_mapping = {user.id: user for user in instance}
+ object_ids = set()
+
+ for data in validated_data:
+ try:
+ if data["id"] in object_ids:
+ # If request data contains users with same ID.
+ raise ValidationError(
+ {"id": [f"User with ID {data['id']} given multiple times."]}
+ )
+ except KeyError:
+ # If user ID not provided in request body.
+ raise ValidationError(
+ {"id": ["This field is required."]}
+ )
+ object_ids.add(data["id"])
+
+ # filter queryset
+ filtered_instances = queryset.filter(id__in=object_ids)
+
+ instance_mapping = {user.id: user for user in filtered_instances}
updated = []
fields_to_update = set()
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 9ddd13d4..3e4b627e 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -6,7 +6,7 @@ from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
from rest_framework.request import Request
from rest_framework.response import Response
-from rest_framework.serializers import ModelSerializer, ValidationError
+from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from pydis_site.apps.api.models.bot.user import User
@@ -210,26 +210,8 @@ class UserViewSet(ModelViewSet):
@action(detail=False, methods=["PATCH"], name='user-bulk-patch')
def bulk_patch(self, request: Request) -> Response:
"""Update multiple User objects in a single request."""
- queryset = self.get_queryset()
- object_ids = set()
- for data in request.data:
- try:
- if data["id"] in object_ids:
- # If request data contains users with same ID.
- raise ValidationError(
- {"id": [f"User with ID {data['id']} given multiple times."]}
- )
- except KeyError:
- # If user ID not provided in request body.
- raise ValidationError(
- {"id": ["This field is required."]}
- )
- object_ids.add(data["id"])
-
- filtered_instances = queryset.filter(id__in=object_ids)
-
serializer = self.get_serializer(
- instance=filtered_instances,
+ instance=self.get_queryset(),
data=request.data,
many=True,
partial=True
--
cgit v1.2.3