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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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 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/apps/api')
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 46ef497fd028531ffac8ebc19dec41a3272f72a3 Mon Sep 17 00:00:00 2001
From: ks123
Date: Mon, 30 Mar 2020 13:30:00 +0300
Subject: (Off-topic Channel Names): Added new field to model: `used` that show
is this name already used on this round of names, added migration for this.
---
.../api/migrations/0051_offtopicchannelname_used.py | 18 ++++++++++++++++++
.../apps/api/models/bot/off_topic_channel_name.py | 5 +++++
2 files changed, 23 insertions(+)
create mode 100644 pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py
new file mode 100644
index 00000000..74836d8c
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0051_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', '0050_remove_infractions_active_default_value'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='offtopicchannelname',
+ name='used',
+ field=models.BooleanField(default=False, help_text='Show is channel already used as channel name in this round.'),
+ ),
+ ]
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 29280c27..3345754d 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="Show is channel already used as channel name in this round."
+ )
+
def __str__(self):
"""Returns the current off-topic name, for display purposes."""
return self.name
--
cgit v1.2.3
From a3d2938d63443ee6fa43751f42c935b22d4efb47 Mon Sep 17 00:00:00 2001
From: ks123
Date: Mon, 30 Mar 2020 17:28:44 +0300
Subject: (Off-topic Channel Names Viewset): Added documentation about new
`mark_used` query parameter, added implementation of this param.
---
.../api/viewsets/bot/off_topic_channel_name.py | 29 ++++++++++++++++++++++
1 file changed, 29 insertions(+)
(limited to 'pydis_site/apps/api')
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..4328c894 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
@@ -21,6 +21,10 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
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.
+ If the `mark_used` query parameter is given like...
+ $ curl api.pydis.local:8000/bot/off-topic-channel-names?random_items=5&mark_used=true
+ ... then the API will mark returned `5` items `used`.
+ When running out of names, API will mark all names to not used and start new round.
#### Response format
Return a list of off-topic-channel names:
@@ -106,6 +110,31 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
'random_items': ["Must be a positive integer."]
})
+ if 'mark_used' in request.query_params and request.query_params['mark_used']:
+ queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count]
+ self.get_queryset().filter(
+ name__in=(query.name for query in queryset)
+ ).update(used=True)
+
+ # When client request more channel names than non-used names is available, start
+ # new round of names.
+ if len(queryset) < random_count:
+ # Get how much names still missing and don't fetch duplicate names.
+ need_more = random_count - len(queryset)
+ ext = self.get_queryset().order_by('?').exclude(
+ name__in=(query.name for query in queryset)
+ )[:need_more]
+
+ # Set all names `used` field to False except these that we just used.
+ self.get_queryset().exclude(name__in=(
+ query.name for query in ext)
+ ).update(used=False)
+ # Join original queryset (that had missing names)
+ # and extension with these missing names.
+ queryset = list(queryset) + list(ext)
+ serialized = self.serializer_class(queryset, many=True)
+ return Response(serialized.data)
+
queryset = self.get_queryset().order_by('?')[:random_count]
serialized = self.serializer_class(queryset, many=True)
return Response(serialized.data)
--
cgit v1.2.3
From 81516be86a22ba1ea6df419f0e4b4277adecae09 Mon Sep 17 00:00:00 2001
From: ks123
Date: Tue, 31 Mar 2020 13:44:38 +0300
Subject: (Off-topic Channel Names Viewset Tests): Added test for not
authenticated request.
---
pydis_site/apps/api/tests/test_off_topic_channel_names.py | 6 ++++++
1 file changed, 6 insertions(+)
(limited to 'pydis_site/apps/api')
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..9697f1f6 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
@@ -21,6 +21,12 @@ class UnauthenticatedTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 401)
+ def test_cannot_read_off_topic_channel_name_list_with_random_item_and_mark_used_param(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=no&mark_used=true')
+
+ self.assertEqual(response.status_code, 401)
+
class EmptyDatabaseTests(APISubdomainTestCase):
def test_returns_empty_object(self):
--
cgit v1.2.3
From 1f66694835013b1025a1630afe3290adbc03b94f Mon Sep 17 00:00:00 2001
From: ks123
Date: Tue, 31 Mar 2020 13:45:46 +0300
Subject: (Off-topic Channel Names Viewset Tests): Added used parameter to list
tests.
---
pydis_site/apps/api/tests/test_off_topic_channel_names.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site/apps/api')
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 9697f1f6..fea3932b 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
@@ -65,8 +65,8 @@ 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):
url = reverse('bot:offtopicchannelname-list', host='api')
--
cgit v1.2.3
From 02315e7339858ae6a11bc742446a14b7220894ca Mon Sep 17 00:00:00 2001
From: ks123
Date: Tue, 31 Mar 2020 13:50:39 +0300
Subject: (Off-topic Channel Names Viewset Tests): Added test for default
handling `mark_used` parameter.
---
pydis_site/apps/api/tests/test_off_topic_channel_names.py | 7 +++++++
1 file changed, 7 insertions(+)
(limited to 'pydis_site/apps/api')
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 fea3932b..be0d5001 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
@@ -88,6 +88,13 @@ class ListTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
+ def test_returns_single_correct_item_with_mark_used_parameter_true_and_random_items_1(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=1&mark_used=true')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [self.test_name.name])
+
class CreationTests(APISubdomainTestCase):
def setUp(self):
--
cgit v1.2.3
From d6f657143fa38657fb0432b9adb779969e920c64 Mon Sep 17 00:00:00 2001
From: ks123
Date: Tue, 31 Mar 2020 13:54:59 +0300
Subject: (Off-topic Channel Names Viewset Tests): Added test for handling
running out of names.
---
pydis_site/apps/api/tests/test_off_topic_channel_names.py | 7 +++++++
1 file changed, 7 insertions(+)
(limited to 'pydis_site/apps/api')
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 be0d5001..06624d89 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
@@ -95,6 +95,13 @@ class ListTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [self.test_name.name])
+ def test_running_out_of_names_with_mark_used_parameter(self):
+ url = reverse('bot:offtopicchannelname-list', host='api')
+ response = self.client.get(f'{url}?random_items=2&mark_used=true')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name])
+
class CreationTests(APISubdomainTestCase):
def setUp(self):
--
cgit v1.2.3
From e321a25c05875e82073470429708b37849947b16 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 28 May 2020 08:23:14 +0300
Subject: OT: Replace help text of `used` field
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Leon Sandรธy
---
pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py | 2 +-
pydis_site/apps/api/models/bot/off_topic_channel_name.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py
index 74836d8c..1b838aec 100644
--- a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py
+++ b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='offtopicchannelname',
name='used',
- field=models.BooleanField(default=False, help_text='Show is channel already used as channel name in this round.'),
+ 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/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py
index 3345754d..413cbfae 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
@@ -18,7 +18,7 @@ class OffTopicChannelName(ModelReprMixin, models.Model):
used = models.BooleanField(
default=False,
- help_text="Show is channel already used as channel name in this round."
+ help_text="Whether or not this name has already been used during this rotation",
)
def __str__(self):
--
cgit v1.2.3
From 1e3501b2f27d7e09a873dc3348d196ac2edda68b Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 28 May 2020 08:49:49 +0300
Subject: OT: Fix migrations nr conflict
---
.../api/migrations/0051_offtopicchannelname_used.py | 18 ------------------
.../api/migrations/0052_offtopicchannelname_used.py | 18 ++++++++++++++++++
2 files changed, 18 insertions(+), 18 deletions(-)
delete mode 100644 pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py
create mode 100644 pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py
deleted file mode 100644
index 1b838aec..00000000
--- a/pydis_site/apps/api/migrations/0051_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', '0050_remove_infractions_active_default_value'),
- ]
-
- 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_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'),
+ ),
+ ]
--
cgit v1.2.3
From 7d5e1f9c60007b230fecdb2b649c5574462fdeb1 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 28 May 2020 09:06:21 +0300
Subject: OT: Refactor off-topic-names random items getting
Remove `mark_used` parameter and move this functionality
to `random_items` parameter. Update docstring of class
---
.../api/viewsets/bot/off_topic_channel_name.py | 55 ++++++++++------------
1 file changed, 25 insertions(+), 30 deletions(-)
(limited to 'pydis_site/apps/api')
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 4328c894..e6cf8172 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
@@ -20,11 +20,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.
- If the `mark_used` query parameter is given like...
- $ curl api.pydis.local:8000/bot/off-topic-channel-names?random_items=5&mark_used=true
- ... then the API will mark returned `5` items `used`.
- When running out of names, API will mark all names to not used and start new round.
+ ... 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:
@@ -110,32 +108,29 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
'random_items': ["Must be a positive integer."]
})
- if 'mark_used' in request.query_params and request.query_params['mark_used']:
- queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count]
- self.get_queryset().filter(
+ queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count]
+ self.get_queryset().filter(
+ name__in=(query.name for query in queryset)
+ ).update(used=True)
+
+ # When client request more channel names than non-used names is available, start
+ # new round of names.
+ if len(queryset) < random_count:
+ # Get how much names still missing and don't fetch duplicate names.
+ need_more = random_count - len(queryset)
+ ext = self.get_queryset().order_by('?').exclude(
name__in=(query.name for query in queryset)
- ).update(used=True)
-
- # When client request more channel names than non-used names is available, start
- # new round of names.
- if len(queryset) < random_count:
- # Get how much names still missing and don't fetch duplicate names.
- need_more = random_count - len(queryset)
- ext = self.get_queryset().order_by('?').exclude(
- name__in=(query.name for query in queryset)
- )[:need_more]
-
- # Set all names `used` field to False except these that we just used.
- self.get_queryset().exclude(name__in=(
- query.name for query in ext)
- ).update(used=False)
- # Join original queryset (that had missing names)
- # and extension with these missing names.
- queryset = list(queryset) + list(ext)
- serialized = self.serializer_class(queryset, many=True)
- return Response(serialized.data)
-
- queryset = self.get_queryset().order_by('?')[:random_count]
+ )[:need_more]
+
+ # Set all names `used` field to False except these that we just used.
+ self.get_queryset().exclude(name__in=(
+ query.name for query in ext)
+ ).update(used=False)
+
+ # Join original queryset (that had missing names)
+ # and extension with these missing names.
+ queryset = list(queryset) + list(ext)
+
serialized = self.serializer_class(queryset, many=True)
return Response(serialized.data)
--
cgit v1.2.3
From 8584aa2a409922c60b9c4a8e0874bc92cd01b9a1 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 28 May 2020 09:12:03 +0300
Subject: OT Tests: Refactor tests to latest change
---
.../apps/api/tests/test_off_topic_channel_names.py | 16 ++--------------
1 file changed, 2 insertions(+), 14 deletions(-)
(limited to 'pydis_site/apps/api')
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 06624d89..51372651 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
@@ -21,12 +21,6 @@ class UnauthenticatedTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 401)
- def test_cannot_read_off_topic_channel_name_list_with_random_item_and_mark_used_param(self):
- url = reverse('bot:offtopicchannelname-list', host='api')
- response = self.client.get(f'{url}?random_items=no&mark_used=true')
-
- self.assertEqual(response.status_code, 401)
-
class EmptyDatabaseTests(APISubdomainTestCase):
def test_returns_empty_object(self):
@@ -87,17 +81,11 @@ class ListTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
-
- def test_returns_single_correct_item_with_mark_used_parameter_true_and_random_items_1(self):
- url = reverse('bot:offtopicchannelname-list', host='api')
- response = self.client.get(f'{url}?random_items=1&mark_used=true')
-
- self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [self.test_name.name])
- def test_running_out_of_names_with_mark_used_parameter(self):
+ def test_running_out_of_names_with_random_parameter(self):
url = reverse('bot:offtopicchannelname-list', host='api')
- response = self.client.get(f'{url}?random_items=2&mark_used=true')
+ 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])
--
cgit v1.2.3
From 72e3ee0645d8017bfdac27f9fd876137164f3e3e Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 28 May 2020 09:30:05 +0300
Subject: OT Tests: Add docstring to tests
---
pydis_site/apps/api/tests/test_off_topic_channel_names.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
(limited to 'pydis_site/apps/api')
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 51372651..cac9405a 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):
+ """Test does this return 401 response code 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):
+ """Test does this give 401 code 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):
+ """Test does this 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):
+ """Test does this 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):
+ """Test does this 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):
+ """Test does this 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')
@@ -63,6 +69,7 @@ class ListTests(APISubdomainTestCase):
cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True)
def test_returns_name_in_list(self):
+ """Test does this return all off-topic channel names."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -76,6 +83,7 @@ class ListTests(APISubdomainTestCase):
)
def test_returns_single_item_with_random_items_param_set_to_1(self):
+ """Test does this return not-used name instead used."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(f'{url}?random_items=1')
@@ -84,6 +92,7 @@ class ListTests(APISubdomainTestCase):
self.assertEqual(response.json(), [self.test_name.name])
def test_running_out_of_names_with_random_parameter(self):
+ """Test does this 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')
@@ -101,6 +110,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
def test_returns_201_for_unicode_chars(self):
+ """Test does this accept all valid characters."""
url = reverse('bot:offtopicchannelname-list', host='api')
names = (
'๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น',
@@ -112,6 +122,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
def test_returns_400_for_missing_name_param(self):
+ """Test does this 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)
@@ -120,6 +131,7 @@ class CreationTests(APISubdomainTestCase):
})
def test_returns_400_for_bad_name_param(self):
+ """Test does this return error message when invalid characters provided."""
url = reverse('bot:offtopicchannelname-list', host='api')
invalid_names = (
'space between words',
@@ -142,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):
+ """Test does this return 404 code 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):
+ """Test does this return 204 code 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):
+ """Test does name gets actually deleted."""
url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api')
response = self.client.delete(url)
--
cgit v1.2.3
From 5c53af373f9f868c5603a903a2c2d9636c2d0982 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 28 May 2020 09:35:17 +0300
Subject: OT: Fix comments
---
pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
(limited to 'pydis_site/apps/api')
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 e6cf8172..c4520a48 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
@@ -113,16 +113,16 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
name__in=(query.name for query in queryset)
).update(used=True)
- # When client request more channel names than non-used names is available, start
- # new round of names.
+ # When the client requests more channel names than are available,
+ # we reset all names to used=False and start a new round of names.
if len(queryset) < random_count:
- # Get how much names still missing and don't fetch duplicate names.
+ # Figure out how many additional names we need, and don't fetch duplicate names.
need_more = random_count - len(queryset)
ext = self.get_queryset().order_by('?').exclude(
name__in=(query.name for query in queryset)
)[:need_more]
- # Set all names `used` field to False except these that we just used.
+ # Reset the `used` field to False for all names except the ones we just used.
self.get_queryset().exclude(name__in=(
query.name for query in ext)
).update(used=False)
--
cgit v1.2.3
From e046ac1d764d80a6a04c3c2d70fb8b4d718b6210 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 28 May 2020 09:36:24 +0300
Subject: OT: Rename variable `need_more` to `names_needed`
---
pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site/apps/api')
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 c4520a48..1099922c 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
@@ -117,10 +117,10 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
# we reset all names to used=False and start a new round of names.
if len(queryset) < random_count:
# Figure out how many additional names we need, and don't fetch duplicate names.
- need_more = random_count - len(queryset)
+ names_needed = random_count - len(queryset)
ext = self.get_queryset().order_by('?').exclude(
name__in=(query.name for query in queryset)
- )[:need_more]
+ )[:names_needed]
# Reset the `used` field to False for all names except the ones we just used.
self.get_queryset().exclude(name__in=(
--
cgit v1.2.3
From 9cedba3870947ca10218c65c4106f5dd095d230c Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Thu, 28 May 2020 09:37:29 +0300
Subject: OT: Rename variable `ext` to `other_names`
---
pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
(limited to 'pydis_site/apps/api')
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 1099922c..9af69ae4 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
@@ -118,18 +118,18 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
if len(queryset) < random_count:
# Figure out how many additional names we need, and don't fetch duplicate names.
names_needed = random_count - len(queryset)
- ext = self.get_queryset().order_by('?').exclude(
+ other_names = self.get_queryset().order_by('?').exclude(
name__in=(query.name for query in queryset)
)[:names_needed]
# Reset the `used` field to False for all names except the ones we just used.
self.get_queryset().exclude(name__in=(
- query.name for query in ext)
+ query.name for query in other_names)
).update(used=False)
# Join original queryset (that had missing names)
# and extension with these missing names.
- queryset = list(queryset) + list(ext)
+ queryset = list(queryset) + list(other_names)
serialized = self.serializer_class(queryset, many=True)
return Response(serialized.data)
--
cgit v1.2.3
From 518a39b3e2b56f942328695d5e44674976317c7c Mon Sep 17 00:00:00 2001
From: Dennis Pham
Date: Sat, 8 Aug 2020 14:46:04 -0400
Subject: Update rule 6 for the removal of #show-your-projects
---
pydis_site/apps/api/views.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py
index a73d4718..7ac56641 100644
--- a/pydis_site/apps/api/views.py
+++ b/pydis_site/apps/api/views.py
@@ -140,7 +140,8 @@ class RulesView(APIView):
),
(
"No spamming or unapproved advertising, including requests for paid work. "
- "Open-source projects can be showcased in #show-your-projects."
+ "Open-source projects can be shared with others in #python-general and "
+ "code reviews can be asked for in a help channel."
),
(
"Keep discussions relevant to channel topics and guidelines."
--
cgit v1.2.3
From 0ba6e25f88400e9a54ae9be2c176687ad69bb94f Mon Sep 17 00:00:00 2001
From: Numerlor <25886452+Numerlor@users.noreply.github.com>
Date: Thu, 20 Aug 2020 15:15:21 +0200
Subject: Allow direct fetching of reminders by id
---
pydis_site/apps/api/tests/test_reminders.py | 28 ++++++++++++++++++++++++++++
pydis_site/apps/api/viewsets/bot/reminder.py | 8 +++++++-
2 files changed, 35 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py
index a05d9296..9dffb668 100644
--- a/pydis_site/apps/api/tests/test_reminders.py
+++ b/pydis_site/apps/api/tests/test_reminders.py
@@ -163,6 +163,34 @@ class ReminderListTests(APISubdomainTestCase):
self.assertEqual(response.json(), [self.rem_dict_one])
+class ReminderRetrieveTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.author = User.objects.create(
+ id=6789,
+ name='Reminder author',
+ discriminator=6789,
+ )
+
+ cls.reminder = Reminder.objects.create(
+ author=cls.author,
+ content="Reminder content",
+ expiration=datetime.utcnow().isoformat(),
+ jump_url="http://example.com/",
+ channel_id=123
+ )
+
+ def test_retrieve_unknown_returns_404(self):
+ url = reverse('bot:reminder-detail', args=("not_an_id",), host='api')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 404)
+
+ def test_retrieve_known_returns_200(self):
+ url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+
class ReminderUpdateTests(APISubdomainTestCase):
@classmethod
def setUpTestData(cls):
diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py
index 6f8a28f2..00413fb1 100644
--- a/pydis_site/apps/api/viewsets/bot/reminder.py
+++ b/pydis_site/apps/api/viewsets/bot/reminder.py
@@ -4,6 +4,7 @@ from rest_framework.mixins import (
CreateModelMixin,
DestroyModelMixin,
ListModelMixin,
+ RetrieveModelMixin,
UpdateModelMixin
)
from rest_framework.viewsets import GenericViewSet
@@ -13,7 +14,12 @@ from pydis_site.apps.api.serializers import ReminderSerializer
class ReminderViewSet(
- CreateModelMixin, ListModelMixin, DestroyModelMixin, UpdateModelMixin, GenericViewSet
+ CreateModelMixin,
+ RetrieveModelMixin,
+ ListModelMixin,
+ DestroyModelMixin,
+ UpdateModelMixin,
+ GenericViewSet,
):
"""
View providing CRUD access to reminders.
--
cgit v1.2.3
From 700de6b08b70f40dc97f3644d4e0ee32ab8f6ccf Mon Sep 17 00:00:00 2001
From: Numerlor <25886452+Numerlor@users.noreply.github.com>
Date: Thu, 20 Aug 2020 15:24:29 +0200
Subject: Update docstring for new fetching behaviour
---
pydis_site/apps/api/viewsets/bot/reminder.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py
index 00413fb1..111660d9 100644
--- a/pydis_site/apps/api/viewsets/bot/reminder.py
+++ b/pydis_site/apps/api/viewsets/bot/reminder.py
@@ -50,6 +50,30 @@ class ReminderViewSet(
#### Status codes
- 200: returned on success
+ ### GET /bot/reminders/
+ Fetches the reminder with the given id.
+
+ #### Response format
+ >>>
+ ... {
+ ... 'active': True,
+ ... 'author': 1020103901030,
+ ... 'mentions': [
+ ... 336843820513755157,
+ ... 165023948638126080,
+ ... 267628507062992896
+ ... ],
+ ... 'content': "Make dinner",
+ ... 'expiration': '5018-11-20T15:52:00Z',
+ ... 'id': 11,
+ ... 'channel_id': 634547009956872193,
+ ... 'jump_url': "https://discord.com/channels///"
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 404: returned when the reminder doesn't exist
+
### POST /bot/reminders
Create a new reminder.
--
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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 32b76b192d72f4235a64b8024131988b4e4c0c36 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 29 Aug 2020 08:55:57 +0300
Subject: Simplify non-random off-topic names selection
---
.../api/viewsets/bot/off_topic_channel_name.py | 39 ++++++++++------------
1 file changed, 17 insertions(+), 22 deletions(-)
(limited to 'pydis_site/apps/api')
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 9af69ae4..29978015 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, When, Value
from django.db.models.query import QuerySet
from django.http.request import HttpRequest
from django.shortcuts import get_object_or_404
@@ -108,28 +109,22 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
'random_items': ["Must be a positive integer."]
})
- queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count]
- self.get_queryset().filter(
- name__in=(query.name for query in queryset)
- ).update(used=True)
-
- # When the client requests more channel names than are available,
- # we reset all names to used=False and start a new round of names.
- if len(queryset) < random_count:
- # Figure out how many additional names we need, and don't fetch duplicate names.
- names_needed = random_count - len(queryset)
- other_names = self.get_queryset().order_by('?').exclude(
- name__in=(query.name for query in queryset)
- )[:names_needed]
-
- # Reset the `used` field to False for all names except the ones we just used.
- self.get_queryset().exclude(name__in=(
- query.name for query in other_names)
- ).update(used=False)
-
- # Join original queryset (that had missing names)
- # and extension with these missing names.
- queryset = list(queryset) + list(other_names)
+ 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):
+ self.get_queryset().update(
+ used=Case( # These names that we just got have to be excluded from updating to False
+ 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 d5945b3d82124214d1bbbf8e156f3516fd3a70cc Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 29 Aug 2020 09:06:11 +0300
Subject: Fix off-topic names test docstrings mood
---
.../apps/api/tests/test_off_topic_channel_names.py | 30 +++++++++++-----------
1 file changed, 15 insertions(+), 15 deletions(-)
(limited to 'pydis_site/apps/api')
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 cac9405a..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,14 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase):
self.client.force_authenticate(user=None)
def test_cannot_read_off_topic_channel_name_list(self):
- """Test does this return 401 response code when not authenticated."""
+ """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):
- """Test does this give 401 code when `random_items` provided and not authenticated."""
+ """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')
@@ -26,7 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase):
class EmptyDatabaseTests(APISubdomainTestCase):
def test_returns_empty_object(self):
- """Test does this return empty list when no names in database."""
+ """Return empty list when no names in database."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -34,7 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
self.assertEqual(response.json(), [])
def test_returns_empty_list_with_get_all_param(self):
- """Test does this return empty list when no names and `random_items` param provided."""
+ """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')
@@ -42,7 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
self.assertEqual(response.json(), [])
def test_returns_400_for_bad_random_items_param(self):
- """Test does this return error message when passing not integer as `random_items`."""
+ """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')
@@ -52,7 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase):
})
def test_returns_400_for_negative_random_items_param(self):
- """Test does this return error message when passing negative int as `random_items`."""
+ """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')
@@ -69,7 +69,7 @@ class ListTests(APISubdomainTestCase):
cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True)
def test_returns_name_in_list(self):
- """Test does this return all off-topic channel names."""
+ """Return all off-topic channel names."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(url)
@@ -83,7 +83,7 @@ class ListTests(APISubdomainTestCase):
)
def test_returns_single_item_with_random_items_param_set_to_1(self):
- """Test does this return not-used name instead used."""
+ """Return not-used name instead used."""
url = reverse('bot:offtopicchannelname-list', host='api')
response = self.client.get(f'{url}?random_items=1')
@@ -92,7 +92,7 @@ class ListTests(APISubdomainTestCase):
self.assertEqual(response.json(), [self.test_name.name])
def test_running_out_of_names_with_random_parameter(self):
- """Test does this reset names `used` parameter to `False` when running out of names."""
+ """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')
@@ -110,7 +110,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
def test_returns_201_for_unicode_chars(self):
- """Test does this accept all valid characters."""
+ """Accept all valid characters."""
url = reverse('bot:offtopicchannelname-list', host='api')
names = (
'๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น',
@@ -122,7 +122,7 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
def test_returns_400_for_missing_name_param(self):
- """Test does this return error message when name not provided."""
+ """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)
@@ -131,7 +131,7 @@ class CreationTests(APISubdomainTestCase):
})
def test_returns_400_for_bad_name_param(self):
- """Test does this return error message when invalid characters provided."""
+ """Return error message when invalid characters provided."""
url = reverse('bot:offtopicchannelname-list', host='api')
invalid_names = (
'space between words',
@@ -154,21 +154,21 @@ class DeletionTests(APISubdomainTestCase):
cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')
def test_deleting_unknown_name_returns_404(self):
- """Test does this return 404 code when trying to delete unknown name."""
+ """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):
- """Test does this return 204 code when deleting was successful."""
+ """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):
- """Test does name gets actually deleted."""
+ """Name gets actually deleted."""
url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api')
response = self.client.delete(url)
--
cgit v1.2.3
From 32342aca3309b4fd482841cb84e4f166152d5c33 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 29 Aug 2020 09:17:15 +0300
Subject: Fix mess with migrations
---
.../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, 417 insertions(+), 445 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_offtopicchannelname_used.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/apps/api')
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_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py
deleted file mode 100644
index dfdf3835..00000000
--- a/pydis_site/apps/api/migrations/0052_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', '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
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)
+ ]
--
cgit v1.2.3
From ac7745e4ea79fc129447e9b472d3dbd49bf00fd6 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 29 Aug 2020 09:21:15 +0300
Subject: Fix linting issues on off-topic viewset
---
pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
(limited to 'pydis_site/apps/api')
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 29978015..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,4 +1,4 @@
-from django.db.models import Case, When, Value
+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
@@ -114,9 +114,13 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
# 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( # These names that we just got have to be excluded from updating to False
- When(name__in=(offtopic_name.name for offtopic_name in queryset), then=Value(True)),
+ used=Case(
+ When(
+ name__in=(offtopic_name.name for offtopic_name in queryset),
+ then=Value(True)
+ ),
default=Value(False)
)
)
--
cgit v1.2.3
From dae68c46831ef14e6a0715add1025f9af1b5a73d Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Sat, 29 Aug 2020 10:00:45 -0700
Subject: Revert pull request #348
Revert commit 330a27926a7f2a9bdae56a5928cd81e55ccb38ed to reverse
changes made to 6c08da2f3e48db9c9dd452f2e0505c4a61e46592. Migrations
were failing when those changes were deployed.
---
.../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, 431 insertions(+), 472 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_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/apps/api')
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_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)
- ]
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 403c7465..20e77b9f 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,11 +16,6 @@ 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 3ab8b22d..bd42cd81 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,14 +10,12 @@ 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')
@@ -26,7 +24,6 @@ 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)
@@ -34,7 +31,6 @@ 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')
@@ -42,7 +38,6 @@ 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')
@@ -52,7 +47,6 @@ 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')
@@ -65,11 +59,10 @@ class EmptyDatabaseTests(APISubdomainTestCase):
class ListTests(APISubdomainTestCase):
@classmethod
def setUpTestData(cls):
- 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)
+ cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand')
+ cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk')
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)
@@ -83,21 +76,11 @@ 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):
@@ -110,7 +93,6 @@ 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 = (
'๐ ๐ก๐ข๐ฃ๐ค๐ฅ๐ฆ๐ง๐จ๐ฉ๐ช๐ซ๐ฌ๐ญ๐ฎ๐ฏ๐ฐ๐ฑ๐ฒ๐ณ๐ด๐ต๐ถ๐ท๐ธ๐น',
@@ -122,7 +104,6 @@ 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)
@@ -131,7 +112,6 @@ 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',
@@ -154,21 +134,18 @@ 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 826ad25e..d6da2399 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,4 +1,3 @@
-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
@@ -21,9 +20,7 @@ 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
- that is not used in current rotation.
- When running out of names, API will mark all names to not used and start new rotation.
+ ... then the API will return `5` random items from the database.
#### Response format
Return a list of off-topic-channel names:
@@ -109,27 +106,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet):
'random_items': ["Must be a positive integer."]
})
- 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)
-
+ queryset = self.get_queryset().order_by('?')[:random_count]
serialized = self.serializer_class(queryset, many=True)
return Response(serialized.data)
--
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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 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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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 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/apps/api')
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 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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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 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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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 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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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 c8c6cb8754bd0917e35eb157925028a9b6f1dcb9 Mon Sep 17 00:00:00 2001
From: Lucas Lindstrรถm
Date: Tue, 6 Oct 2020 21:29:34 +0200
Subject: Added metricity db connection and user bot API
---
docker-compose.yml | 3 +++
postgres/init.sql | 34 +++++++++++++++++++++++
pydis_site/apps/api/viewsets/bot/user.py | 46 ++++++++++++++++++++++++++++++++
pydis_site/settings.py | 3 ++-
4 files changed, 85 insertions(+), 1 deletion(-)
create mode 100644 postgres/init.sql
(limited to 'pydis_site/apps/api')
diff --git a/docker-compose.yml b/docker-compose.yml
index 73d2ff85..7287d8d5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,6 +18,8 @@ services:
POSTGRES_DB: pysite
POSTGRES_PASSWORD: pysite
POSTGRES_USER: pysite
+ volumes:
+ - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
web:
build:
@@ -40,6 +42,7 @@ services:
- staticfiles:/var/www/static
environment:
DATABASE_URL: postgres://pysite:pysite@postgres:5432/pysite
+ METRICITY_DB_URL: postgres://pysite:pysite@postgres:5432/metricity
SECRET_KEY: suitable-for-development-only
STATIC_ROOT: /var/www/static
diff --git a/postgres/init.sql b/postgres/init.sql
new file mode 100644
index 00000000..fd29ddbc
--- /dev/null
+++ b/postgres/init.sql
@@ -0,0 +1,34 @@
+CREATE DATABASE metricity;
+
+\c metricity;
+
+CREATE TABLE users (
+ id varchar(255),
+ name varchar(255) not null,
+ avatar_hash varchar(255),
+ joined_at timestamp not null,
+ created_at timestamp not null,
+ is_staff boolean not null,
+ opt_out boolean default false,
+ bot boolean default false,
+ is_guild boolean default true,
+ is_verified boolean default false,
+ public_flags text default '{}',
+ verified_at timestamp,
+ primary key(id)
+);
+
+INSERT INTO users VALUES (
+ 0,
+ 'foo',
+ 'bar',
+ current_timestamp,
+ current_timestamp,
+ false,
+ false,
+ false,
+ true,
+ false,
+ '{}',
+ NULL
+);
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 9571b3d7..0eeacbb3 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,3 +1,10 @@
+import json
+
+from django.db import connections
+from rest_framework import status
+from rest_framework.decorators import action
+from rest_framework.request import Request
+from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework_bulk import BulkCreateModelMixin
@@ -53,6 +60,29 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
- 200: returned on success
- 404: if a user with the given `snowflake` could not be found
+ ### GET /bot/users//metricity_data
+ Gets metricity data for a single user by ID.
+
+ #### Response format
+ >>> {
+ ... "id": "0",
+ ... "name": "foo",
+ ... "avatar_hash": "bar",
+ ... "joined_at": "2020-10-06T18:17:30.101677",
+ ... "created_at": "2020-10-06T18:17:30.101677",
+ ... "is_staff": False,
+ ... "opt_out": False,
+ ... "bot": False,
+ ... "is_guild": True,
+ ... "is_verified": False,
+ ... "public_flags": {},
+ ... "verified_at": null
+ ...}
+
+ #### Status codes
+ - 200: returned on success
+ - 404: if a user with the given `snowflake` could not be found
+
### POST /bot/users
Adds a single or multiple new users.
The roles attached to the user(s) must be roles known by the site.
@@ -115,7 +145,23 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
#### Status codes
- 204: returned on success
- 404: if a user with the given `snowflake` does not exist
+
+
"""
serializer_class = UserSerializer
queryset = User.objects
+
+ @action(detail=True)
+ def metricity_data(self, request: Request, pk: str = None) -> Response:
+ """Request handler for metricity_data endpoint."""
+ user = self.get_object()
+ column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff",
+ "opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"]
+ with connections['metricity'].cursor() as cursor:
+ query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'"
+ cursor.execute(query, [user.id])
+ values = cursor.fetchone()
+ data = dict(zip(column_keys, values))
+ data["public_flags"] = json.loads(data["public_flags"])
+ return Response(data, status=status.HTTP_200_OK)
diff --git a/pydis_site/settings.py b/pydis_site/settings.py
index 3769fa25..2e78e458 100644
--- a/pydis_site/settings.py
+++ b/pydis_site/settings.py
@@ -172,7 +172,8 @@ WSGI_APPLICATION = 'pydis_site.wsgi.application'
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
- 'default': env.db()
+ 'default': env.db(),
+ 'metricity': env.db('METRICITY_DB_URL'),
}
# Password validation
--
cgit v1.2.3
From a188ab8ebfa9299addb7fd0effce2de7a0509645 Mon Sep 17 00:00:00 2001
From: Lucas Lindstrรถm
Date: Tue, 6 Oct 2020 22:02:08 +0200
Subject: Fix minor style issue.
---
pydis_site/apps/api/viewsets/bot/user.py | 2 --
1 file changed, 2 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 0eeacbb3..059bc0f0 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -145,8 +145,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
#### Status codes
- 204: returned on success
- 404: if a user with the given `snowflake` does not exist
-
-
"""
serializer_class = UserSerializer
--
cgit v1.2.3
From 15086b4ab392a8bdc6c33414f0c4e2a294f4a2ef Mon Sep 17 00:00:00 2001
From: Lucas Lindstrรถm
Date: Tue, 6 Oct 2020 22:40:14 +0200
Subject: Added total message count to metricity data response.
---
postgres/init.sql | 16 ++++++++++++++++
pydis_site/apps/api/viewsets/bot/user.py | 9 ++++++++-
2 files changed, 24 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/postgres/init.sql b/postgres/init.sql
index fd29ddbc..45ad440c 100644
--- a/postgres/init.sql
+++ b/postgres/init.sql
@@ -32,3 +32,19 @@ INSERT INTO users VALUES (
'{}',
NULL
);
+
+CREATE TABLE messages (
+ id varchar(255),
+ author_id varchar(255) references users(id),
+ primary key(id)
+);
+
+INSERT INTO messages VALUES(
+ 0,
+ 0
+);
+
+INSERT INTO messages VALUES(
+ 1,
+ 0
+);
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 059bc0f0..b3d880cc 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -76,7 +76,8 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
... "is_guild": True,
... "is_verified": False,
... "public_flags": {},
- ... "verified_at": null
+ ... "verified_at": None,
+ ... "total_messages": 2
...}
#### Status codes
@@ -157,9 +158,15 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff",
"opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"]
with connections['metricity'].cursor() as cursor:
+ # Get user data
query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'"
cursor.execute(query, [user.id])
values = cursor.fetchone()
data = dict(zip(column_keys, values))
data["public_flags"] = json.loads(data["public_flags"])
+
+ # Get message count
+ cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id])
+ data["total_messages"], = cursor.fetchone()
+
return Response(data, status=status.HTTP_200_OK)
--
cgit v1.2.3
From e83f445a9b8d2db4523e261759bb73ea83ed54c3 Mon Sep 17 00:00:00 2001
From: Lucas Lindstrรถm
Date: Tue, 6 Oct 2020 23:56:47 +0200
Subject: Reduce metricity db setup script and API response to the bare
necessities.
---
postgres/init.sql | 28 ++++------------------------
pydis_site/apps/api/viewsets/bot/user.py | 29 ++++-------------------------
2 files changed, 8 insertions(+), 49 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/postgres/init.sql b/postgres/init.sql
index 45ad440c..922ce1ad 100644
--- a/postgres/init.sql
+++ b/postgres/init.sql
@@ -3,39 +3,19 @@ CREATE DATABASE metricity;
\c metricity;
CREATE TABLE users (
- id varchar(255),
- name varchar(255) not null,
- avatar_hash varchar(255),
- joined_at timestamp not null,
- created_at timestamp not null,
- is_staff boolean not null,
- opt_out boolean default false,
- bot boolean default false,
- is_guild boolean default true,
- is_verified boolean default false,
- public_flags text default '{}',
+ id varchar,
verified_at timestamp,
primary key(id)
);
INSERT INTO users VALUES (
0,
- 'foo',
- 'bar',
- current_timestamp,
- current_timestamp,
- false,
- false,
- false,
- true,
- false,
- '{}',
- NULL
+ current_timestamp
);
CREATE TABLE messages (
- id varchar(255),
- author_id varchar(255) references users(id),
+ id varchar,
+ author_id varchar references users(id),
primary key(id)
);
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index b3d880cc..1b1af841 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,5 +1,3 @@
-import json
-
from django.db import connections
from rest_framework import status
from rest_framework.decorators import action
@@ -65,18 +63,7 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
#### Response format
>>> {
- ... "id": "0",
- ... "name": "foo",
- ... "avatar_hash": "bar",
- ... "joined_at": "2020-10-06T18:17:30.101677",
- ... "created_at": "2020-10-06T18:17:30.101677",
- ... "is_staff": False,
- ... "opt_out": False,
- ... "bot": False,
- ... "is_guild": True,
- ... "is_verified": False,
- ... "public_flags": {},
- ... "verified_at": None,
+ ... "verified_at": "2020-10-06T21:54:23.540766",
... "total_messages": 2
...}
@@ -155,18 +142,10 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
def metricity_data(self, request: Request, pk: str = None) -> Response:
"""Request handler for metricity_data endpoint."""
user = self.get_object()
- column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff",
- "opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"]
with connections['metricity'].cursor() as cursor:
- # Get user data
- query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'"
- cursor.execute(query, [user.id])
- values = cursor.fetchone()
- data = dict(zip(column_keys, values))
- data["public_flags"] = json.loads(data["public_flags"])
-
- # Get message count
+ data = {}
+ cursor.execute("SELECT verified_at FROM users WHERE id = '%s'", [user.id])
+ data["verified_at"], = cursor.fetchone()
cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id])
data["total_messages"], = cursor.fetchone()
-
return Response(data, status=status.HTTP_200_OK)
--
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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 484eba7715fcbcc195d66f5a60ff56c8167ecf0e Mon Sep 17 00:00:00 2001
From: Lucas Lindstrรถm
Date: Thu, 8 Oct 2020 00:17:47 +0200
Subject: Broke out metricity connection into an abstraction and added
metricity endpoint unit tests.
---
.coveragerc | 1 +
pydis_site/apps/api/models/bot/metricity.py | 42 +++++++++++++++++++++
pydis_site/apps/api/tests/test_users.py | 58 +++++++++++++++++++++++++++++
pydis_site/apps/api/viewsets/bot/user.py | 17 +++++----
4 files changed, 110 insertions(+), 8 deletions(-)
create mode 100644 pydis_site/apps/api/models/bot/metricity.py
(limited to 'pydis_site/apps/api')
diff --git a/.coveragerc b/.coveragerc
index f5ddf08d..0cccc47c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -12,6 +12,7 @@ omit =
*/admin.py
*/apps.py
*/urls.py
+ pydis_site/apps/api/models/bot/metricity.py
pydis_site/wsgi.py
pydis_site/settings.py
pydis_site/utils/resources.py
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
new file mode 100644
index 00000000..25b42fa2
--- /dev/null
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -0,0 +1,42 @@
+from django.db import connections
+
+
+class NotFound(Exception):
+ """Raised when an entity cannot be found."""
+
+ pass
+
+
+class Metricity:
+ """Abstraction for a connection to the metricity database."""
+
+ def __init__(self):
+ self.cursor = connections['metricity'].cursor()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *_):
+ self.cursor.close()
+
+ def user(self, user_id: str) -> dict:
+ """Query a user's data."""
+ columns = ["verified_at"]
+ query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'"
+ self.cursor.execute(query, [user_id])
+ values = self.cursor.fetchone()
+
+ if not values:
+ raise NotFound()
+
+ return dict(zip(columns, values))
+
+ def total_messages(self, user_id: str) -> int:
+ """Query total number of messages for a user."""
+ self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user_id])
+ values = self.cursor.fetchone()
+
+ if not values:
+ raise NotFound()
+
+ return values[0]
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index a02fce8a..76a21d3a 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -1,7 +1,10 @@
+from unittest.mock import patch
+
from django_hosts.resolvers import reverse
from .base import APISubdomainTestCase
from ..models import Role, User
+from ..models.bot.metricity import NotFound
class UnauthedUserAPITests(APISubdomainTestCase):
@@ -170,3 +173,58 @@ 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 UserMetricityTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ User.objects.create(
+ id=0,
+ name="Test user",
+ discriminator=1,
+ in_guild=True,
+ )
+
+ def test_get_metricity_data(self):
+ # Given
+ verified_at = "foo"
+ total_messages = 1
+ self.mock_metricity_user(verified_at, total_messages)
+
+ # When
+ url = reverse('bot:user-metricity-data', args=[0], host='api')
+ response = self.client.get(url)
+
+ # Then
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), {
+ "verified_at": verified_at,
+ "total_messages": total_messages,
+ })
+
+ def test_no_metricity_user(self):
+ # Given
+ self.mock_no_metricity_user()
+
+ # When
+ url = reverse('bot:user-metricity-data', args=[0], host='api')
+ response = self.client.get(url)
+
+ # Then
+ self.assertEqual(response.status_code, 404)
+
+ def mock_metricity_user(self, verified_at, total_messages):
+ patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
+ self.metricity = patcher.start()
+ self.addCleanup(patcher.stop)
+ self.metricity = self.metricity.return_value.__enter__.return_value
+ self.metricity.user.return_value = dict(verified_at=verified_at)
+ self.metricity.total_messages.return_value = total_messages
+
+ def mock_no_metricity_user(self):
+ patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
+ self.metricity = patcher.start()
+ self.addCleanup(patcher.stop)
+ self.metricity = self.metricity.return_value.__enter__.return_value
+ self.metricity.user.side_effect = NotFound()
+ self.metricity.total_messages.side_effect = NotFound()
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 1b1af841..352d77c0 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,4 +1,3 @@
-from django.db import connections
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.request import Request
@@ -6,6 +5,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework_bulk import BulkCreateModelMixin
+from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound
from pydis_site.apps.api.models.bot.user import User
from pydis_site.apps.api.serializers import UserSerializer
@@ -142,10 +142,11 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet):
def metricity_data(self, request: Request, pk: str = None) -> Response:
"""Request handler for metricity_data endpoint."""
user = self.get_object()
- with connections['metricity'].cursor() as cursor:
- data = {}
- cursor.execute("SELECT verified_at FROM users WHERE id = '%s'", [user.id])
- data["verified_at"], = cursor.fetchone()
- cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id])
- data["total_messages"], = cursor.fetchone()
- return Response(data, status=status.HTTP_200_OK)
+ with Metricity() as metricity:
+ try:
+ data = metricity.user(user.id)
+ data["total_messages"] = metricity.total_messages(user.id)
+ return Response(data, status=status.HTTP_200_OK)
+ except NotFound:
+ return Response(dict(detail="User not found in metricity"),
+ status=status.HTTP_404_NOT_FOUND)
--
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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/apps/api')
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
From 9c812d5c40813b453bf0711538eaebea46e2b16b Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 19:09:31 +0300
Subject: Add voice ban to infraction types and create migration for it
---
.../migrations/0067_add_voice_ban_infraction_type.py | 18 ++++++++++++++++++
pydis_site/apps/api/models/bot/infraction.py | 3 ++-
2 files changed, 20 insertions(+), 1 deletion(-)
create mode 100644 pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py
new file mode 100644
index 00000000..9a940ff4
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0067_add_voice_ban_infraction_type.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.10 on 2020-10-10 16:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0066_merge_20201003_0730'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='infraction',
+ name='type',
+ field=models.CharField(choices=[('note', 'Note'), ('warning', 'Warning'), ('watch', 'Watch'), ('mute', 'Mute'), ('kick', 'Kick'), ('ban', 'Ban'), ('superstar', 'Superstar'), ('voice_ban', 'Voice Ban')], help_text='The type of the infraction.', max_length=9),
+ ),
+ ]
diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py
index 7660cbba..5ef3a4ce 100644
--- a/pydis_site/apps/api/models/bot/infraction.py
+++ b/pydis_site/apps/api/models/bot/infraction.py
@@ -15,7 +15,8 @@ class Infraction(ModelReprMixin, models.Model):
("mute", "Mute"),
("kick", "Kick"),
("ban", "Ban"),
- ("superstar", "Superstar")
+ ("superstar", "Superstar"),
+ ("voice_ban", "Voice Ban")
)
inserted_at = models.DateTimeField(
default=timezone.now,
--
cgit v1.2.3
From 511152c0a91ae81949941ff9d8f8129f01338173 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 19:14:36 +0300
Subject: Don't allow voice ban to be hidden infraction
---
pydis_site/apps/api/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 25c5c82e..10eb3839 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -167,7 +167,7 @@ class InfractionSerializer(ModelSerializer):
raise ValidationError({'expires_at': [f'{infr_type} infractions cannot expire.']})
hidden = attrs.get('hidden')
- if hidden and infr_type in ('superstar', 'warning'):
+ if hidden and infr_type in ('superstar', 'warning', 'voice_ban'):
raise ValidationError({'hidden': [f'{infr_type} infractions cannot be hidden.']})
if not hidden and infr_type in ('note', ):
--
cgit v1.2.3
From 990d8cfc98ec71a31ea3ef659cb109547f987e31 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 19:56:54 +0300
Subject: Include voice ban status on metricity data
---
pydis_site/apps/api/viewsets/bot/user.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 3ab71186..19f55555 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,6 +1,7 @@
import typing
from collections import OrderedDict
+from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
@@ -9,6 +10,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
+from pydis_site.apps.api.models.bot.infraction import Infraction
from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound
from pydis_site.apps.api.models.bot.user import User
from pydis_site.apps.api.serializers import UserSerializer
@@ -240,10 +242,21 @@ class UserViewSet(ModelViewSet):
def metricity_data(self, request: Request, pk: str = None) -> Response:
"""Request handler for metricity_data endpoint."""
user = self.get_object()
+
+ try:
+ Infraction.objects.get(user__id=user.id, active=True, type="voice_ban")
+ except ObjectDoesNotExist:
+ voice_banned = False
+ except MultipleObjectReturned:
+ voice_banned = True
+ else:
+ voice_banned = True
+
with Metricity() as metricity:
try:
data = metricity.user(user.id)
data["total_messages"] = metricity.total_messages(user.id)
+ data["voice_banned"] = voice_banned
return Response(data, status=status.HTTP_200_OK)
except NotFound:
return Response(dict(detail="User not found in metricity"),
--
cgit v1.2.3
From f1bdb525af330f28742638fdaa19cd8c76235906 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 20:15:01 +0300
Subject: Cover metricity voice ban status with tests
---
pydis_site/apps/api/tests/test_users.py | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index d03785ae..01bf208b 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -1,5 +1,6 @@
from unittest.mock import patch
+from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django_hosts.resolvers import reverse
from .base import APISubdomainTestCase
@@ -419,6 +420,7 @@ class UserMetricityTests(APISubdomainTestCase):
self.assertEqual(response.json(), {
"verified_at": verified_at,
"total_messages": total_messages,
+ "voice_banned": False,
})
def test_no_metricity_user(self):
@@ -432,6 +434,27 @@ class UserMetricityTests(APISubdomainTestCase):
# Then
self.assertEqual(response.status_code, 404)
+ def test_metricity_voice_banned(self):
+ cases = [
+ {'exception': None, 'voice_banned': True},
+ {'exception': ObjectDoesNotExist, 'voice_banned': False},
+ {'exception': MultipleObjectsReturned, 'voice_banned': True},
+ ]
+
+ self.mock_metricity_user("foo", 1)
+
+ for case in cases:
+ with self.subTest(exception=case['exception'], voice_banned=case['voice_banned']):
+ with patch("pydis_site.apps.api.viewsets.bot.user.Infraction.objects.get") as p:
+ p.side_effect = case['exception']
+
+ url = reverse('bot:user-metricity-data', args=[0], host='api')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json()["voice_banned"], case["voice_banned"])
+
+
def mock_metricity_user(self, verified_at, total_messages):
patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
self.metricity = patcher.start()
--
cgit v1.2.3
From d80c7f18fadd1abc1aa2c080c2de6e7dee395883 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 20:19:53 +0300
Subject: Fix wrong exception name
---
pydis_site/apps/api/viewsets/bot/user.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 19f55555..367f6b65 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -247,7 +247,7 @@ class UserViewSet(ModelViewSet):
Infraction.objects.get(user__id=user.id, active=True, type="voice_ban")
except ObjectDoesNotExist:
voice_banned = False
- except MultipleObjectReturned:
+ except MultipleObjectsReturned:
voice_banned = True
else:
voice_banned = True
--
cgit v1.2.3
From 8cb18e635920d64734a66be77e849cae3441108b Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 20:21:42 +0300
Subject: Remove unnecessary newline from user tests
---
pydis_site/apps/api/tests/test_users.py | 1 -
1 file changed, 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 01bf208b..e512f506 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -454,7 +454,6 @@ class UserMetricityTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["voice_banned"], case["voice_banned"])
-
def mock_metricity_user(self, verified_at, total_messages):
patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
self.metricity = patcher.start()
--
cgit v1.2.3
From 74f2131d05fdaa27b841e32f6c2fce2926b60f70 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 20:34:13 +0300
Subject: Add trailing comma to infraction types listing
Co-authored-by: Joe Banks
---
pydis_site/apps/api/models/bot/infraction.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py
index 5ef3a4ce..60c1e8dd 100644
--- a/pydis_site/apps/api/models/bot/infraction.py
+++ b/pydis_site/apps/api/models/bot/infraction.py
@@ -16,7 +16,7 @@ class Infraction(ModelReprMixin, models.Model):
("kick", "Kick"),
("ban", "Ban"),
("superstar", "Superstar"),
- ("voice_ban", "Voice Ban")
+ ("voice_ban", "Voice Ban"),
)
inserted_at = models.DateTimeField(
default=timezone.now,
--
cgit v1.2.3
From 9c4ed0b98e4f34fe3a589e4e26f2b31e6c5169f9 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 20:35:32 +0300
Subject: Don't catch multiple object returned exception
---
pydis_site/apps/api/viewsets/bot/user.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 367f6b65..5205dc97 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,7 +1,7 @@
import typing
from collections import OrderedDict
-from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
@@ -247,8 +247,6 @@ class UserViewSet(ModelViewSet):
Infraction.objects.get(user__id=user.id, active=True, type="voice_ban")
except ObjectDoesNotExist:
voice_banned = False
- except MultipleObjectsReturned:
- voice_banned = True
else:
voice_banned = True
--
cgit v1.2.3
From 42e34e955724eb47a505e24bcd03dc55c8e66419 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 10 Oct 2020 20:36:18 +0300
Subject: Remove multiple objects returning test case
---
pydis_site/apps/api/tests/test_users.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index e512f506..72ffcb3c 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -1,6 +1,6 @@
from unittest.mock import patch
-from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist
from django_hosts.resolvers import reverse
from .base import APISubdomainTestCase
@@ -438,7 +438,6 @@ class UserMetricityTests(APISubdomainTestCase):
cases = [
{'exception': None, 'voice_banned': True},
{'exception': ObjectDoesNotExist, 'voice_banned': False},
- {'exception': MultipleObjectsReturned, 'voice_banned': True},
]
self.mock_metricity_user("foo", 1)
--
cgit v1.2.3
From 730efe6cff7b61610d97bc6d3401acf5617bd17b Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Sun, 18 Oct 2020 19:38:32 +0100
Subject: Exclude deleted messages from total message count
---
pydis_site/apps/api/models/bot/metricity.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 25b42fa2..cdfbb499 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -33,7 +33,7 @@ class Metricity:
def total_messages(self, user_id: str) -> int:
"""Query total number of messages for a user."""
- self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user_id])
+ self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", [user_id])
values = self.cursor.fetchone()
if not values:
--
cgit v1.2.3
From d0853d28b34789756d03f414dc33fa84bc774142 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Sun, 18 Oct 2020 19:42:39 +0100
Subject: Split query into multiple lines for total messages
---
pydis_site/apps/api/models/bot/metricity.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index cdfbb499..eed1deb4 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -33,7 +33,10 @@ class Metricity:
def total_messages(self, user_id: str) -> int:
"""Query total number of messages for a user."""
- self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted", [user_id])
+ self.cursor.execute(
+ "SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted",
+ [user_id]
+ )
values = self.cursor.fetchone()
if not values:
--
cgit v1.2.3
From 9db412a09da490093a1dfdcc6036b8cd7fa619ee Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Tue, 27 Oct 2020 01:51:27 +0000
Subject: Add message block query
---
pydis_site/apps/api/models/bot/metricity.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index eed1deb4..afbcbad8 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -1,5 +1,7 @@
from django.db import connections
+BLOCK_INTERVAL = 10 * 60 # 10 minute blocks
+
class NotFound(Exception):
"""Raised when an entity cannot be found."""
@@ -43,3 +45,25 @@ class Metricity:
raise NotFound()
return values[0]
+
+ def total_message_blocks(self, user_id: str) -> int:
+ """Query number of 10 minute blocks the user has been active during."""
+ self.cursor.execute(
+ """
+ SELECT
+ COUNT(*)
+ FROM (
+ SELECT
+ to_timestamp(floor((extract('epoch' from created_at) / 600 )) * 600)
+ AT TIME ZONE 'UTC' AS interval
+ FROM messages
+ WHERE author_id='%s' AND NOT is_deleted GROUP BY interval) block_query;
+ """,
+ [user_id]
+ )
+ values = self.cursor.fetchone()
+
+ if not values:
+ raise NotFound()
+
+ return values[0]
--
cgit v1.2.3
From b63d79ac0f663d2062082d8cccf7dc0e072ceeb1 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Tue, 27 Oct 2020 01:51:55 +0000
Subject: Update viewset to include activity_blocks response item
---
pydis_site/apps/api/viewsets/bot/user.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 5205dc97..79f90163 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -110,7 +110,9 @@ class UserViewSet(ModelViewSet):
#### Response format
>>> {
... "verified_at": "2020-10-06T21:54:23.540766",
- ... "total_messages": 2
+ ... "total_messages": 2,
+ ... "voice_banned": False,
+ ... "activity_blocks": 1
...}
#### Status codes
@@ -255,6 +257,7 @@ class UserViewSet(ModelViewSet):
data = metricity.user(user.id)
data["total_messages"] = metricity.total_messages(user.id)
data["voice_banned"] = voice_banned
+ data["activity_blocks"] = metricity.total_message_blocks(user.id)
return Response(data, status=status.HTTP_200_OK)
except NotFound:
return Response(dict(detail="User not found in metricity"),
--
cgit v1.2.3
From 7899fe70ac654bdee58850734c2cb26ba7c04a0d Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Tue, 27 Oct 2020 01:52:02 +0000
Subject: Update tests with new activity_blocks key
---
pydis_site/apps/api/tests/test_users.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index 72ffcb3c..c422f895 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -409,7 +409,8 @@ class UserMetricityTests(APISubdomainTestCase):
# Given
verified_at = "foo"
total_messages = 1
- self.mock_metricity_user(verified_at, total_messages)
+ total_blocks = 1
+ self.mock_metricity_user(verified_at, total_messages, total_blocks)
# When
url = reverse('bot:user-metricity-data', args=[0], host='api')
@@ -421,6 +422,7 @@ class UserMetricityTests(APISubdomainTestCase):
"verified_at": verified_at,
"total_messages": total_messages,
"voice_banned": False,
+ "activity_blocks": total_blocks
})
def test_no_metricity_user(self):
@@ -440,7 +442,7 @@ class UserMetricityTests(APISubdomainTestCase):
{'exception': ObjectDoesNotExist, 'voice_banned': False},
]
- self.mock_metricity_user("foo", 1)
+ self.mock_metricity_user("foo", 1, 1)
for case in cases:
with self.subTest(exception=case['exception'], voice_banned=case['voice_banned']):
@@ -453,13 +455,14 @@ class UserMetricityTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["voice_banned"], case["voice_banned"])
- def mock_metricity_user(self, verified_at, total_messages):
+ def mock_metricity_user(self, verified_at, total_messages, total_blocks):
patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
self.metricity = patcher.start()
self.addCleanup(patcher.stop)
self.metricity = self.metricity.return_value.__enter__.return_value
self.metricity.user.return_value = dict(verified_at=verified_at)
self.metricity.total_messages.return_value = total_messages
+ self.metricity.total_message_blocks.return_value = total_blocks
def mock_no_metricity_user(self):
patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
@@ -468,3 +471,4 @@ class UserMetricityTests(APISubdomainTestCase):
self.metricity = self.metricity.return_value.__enter__.return_value
self.metricity.user.side_effect = NotFound()
self.metricity.total_messages.side_effect = NotFound()
+ self.metricity.total_message_blocks.side_effect = NotFound()
--
cgit v1.2.3
From 2337453ef485017b519c47f31129063e0a0de011 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Tue, 27 Oct 2020 01:57:06 +0000
Subject: Clean up SQL query
---
pydis_site/apps/api/models/bot/metricity.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index afbcbad8..93d0df71 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -57,7 +57,11 @@ class Metricity:
to_timestamp(floor((extract('epoch' from created_at) / 600 )) * 600)
AT TIME ZONE 'UTC' AS interval
FROM messages
- WHERE author_id='%s' AND NOT is_deleted GROUP BY interval) block_query;
+ WHERE
+ author_id='%s'
+ AND NOT is_deleted
+ GROUP BY interval
+ ) block_query;
""",
[user_id]
)
--
cgit v1.2.3
From 8c7eab5966089a64dcf0899c4f5c28462f745cdb Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Tue, 27 Oct 2020 02:11:30 +0000
Subject: Constant was never used :man_facepalming:
---
pydis_site/apps/api/models/bot/metricity.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 93d0df71..81d6e788 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -49,12 +49,15 @@ class Metricity:
def total_message_blocks(self, user_id: str) -> int:
"""Query number of 10 minute blocks the user has been active during."""
self.cursor.execute(
- """
+ f"""
SELECT
COUNT(*)
FROM (
SELECT
- to_timestamp(floor((extract('epoch' from created_at) / 600 )) * 600)
+ to_timestamp(
+ floor((extract('epoch' from created_at) / {BLOCK_INTERVAL} ))
+ * {BLOCK_INTERVAL}
+ )
AT TIME ZONE 'UTC' AS interval
FROM messages
WHERE
--
cgit v1.2.3
From 2ef2a936ca43dd615cf6fb831b868d38abb58bae Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Tue, 27 Oct 2020 02:16:40 +0000
Subject: Use SQL formatting instead of f-strings for injecting values
---
pydis_site/apps/api/models/bot/metricity.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 81d6e788..6f03baf0 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -55,8 +55,8 @@ class Metricity:
FROM (
SELECT
to_timestamp(
- floor((extract('epoch' from created_at) / {BLOCK_INTERVAL} ))
- * {BLOCK_INTERVAL}
+ floor((extract('epoch' from created_at) / %d ))
+ * %d
)
AT TIME ZONE 'UTC' AS interval
FROM messages
@@ -66,7 +66,7 @@ class Metricity:
GROUP BY interval
) block_query;
""",
- [user_id]
+ [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id]
)
values = self.cursor.fetchone()
--
cgit v1.2.3
From a6f3a94cab63b52fd450442bbc0b24fdafbbb580 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Tue, 27 Oct 2020 02:19:54 +0000
Subject: Remove unnecessary f-string marker
---
pydis_site/apps/api/models/bot/metricity.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 6f03baf0..4313d2e2 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -49,7 +49,7 @@ class Metricity:
def total_message_blocks(self, user_id: str) -> int:
"""Query number of 10 minute blocks the user has been active during."""
self.cursor.execute(
- f"""
+ """
SELECT
COUNT(*)
FROM (
--
cgit v1.2.3
From 09d775c57b69c661a8de692c1b4a417331d5ac57 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Tue, 27 Oct 2020 02:43:59 +0000
Subject: Remove unnecessary timestamp conversion
---
pydis_site/apps/api/models/bot/metricity.py | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 4313d2e2..60b2e73d 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -54,11 +54,7 @@ class Metricity:
COUNT(*)
FROM (
SELECT
- to_timestamp(
- floor((extract('epoch' from created_at) / %d ))
- * %d
- )
- AT TIME ZONE 'UTC' AS interval
+ (floor((extract('epoch' from created_at) / %d )) * %d) AS interval
FROM messages
WHERE
author_id='%s'
--
cgit v1.2.3
From 3595f62f00ef8786317c4af1f3522bd13f7656a1 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Fri, 30 Oct 2020 23:57:32 +0000
Subject: Update docstring in metricity block query
---
pydis_site/apps/api/models/bot/metricity.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 60b2e73d..bb1db708 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -47,7 +47,11 @@ class Metricity:
return values[0]
def total_message_blocks(self, user_id: str) -> int:
- """Query number of 10 minute blocks the user has been active during."""
+ """
+ Query number of 10 minute blocks during which the user has been active.
+
+ This metric prevents users from spamming to achieve the message total threshold.
+ """
self.cursor.execute(
"""
SELECT
--
cgit v1.2.3
From 7968dca8d3b6a9ae43e888bc045af211425acb28 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Fri, 30 Oct 2020 23:57:51 +0000
Subject: Indent metricity block query correctly
---
pydis_site/apps/api/models/bot/metricity.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index bb1db708..e7fc92fc 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -55,15 +55,15 @@ class Metricity:
self.cursor.execute(
"""
SELECT
- COUNT(*)
- FROM (
- SELECT
- (floor((extract('epoch' from created_at) / %d )) * %d) AS interval
- FROM messages
- WHERE
- author_id='%s'
- AND NOT is_deleted
- GROUP BY interval
+ COUNT(*)
+ FROM (
+ SELECT
+ (floor((extract('epoch' from created_at) / %d )) * %d) AS interval
+ FROM messages
+ WHERE
+ author_id='%s'
+ AND NOT is_deleted
+ GROUP BY interval
) block_query;
""",
[BLOCK_INTERVAL, BLOCK_INTERVAL, user_id]
--
cgit v1.2.3
From d8e387b40cd6f3987a0815bf46b7b19888e83be9 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Sat, 31 Oct 2020 00:13:08 +0000
Subject: Change format character in metricity data endpoint
---
pydis_site/apps/api/models/bot/metricity.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index e7fc92fc..379b0757 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -58,7 +58,7 @@ class Metricity:
COUNT(*)
FROM (
SELECT
- (floor((extract('epoch' from created_at) / %d )) * %d) AS interval
+ (floor((extract('epoch' from created_at) / %s )) * %s) AS interval
FROM messages
WHERE
author_id='%s'
--
cgit v1.2.3
From 23bf022cf8fae66225e829c4a0abbccb2940edb7 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Mon, 2 Nov 2020 18:25:27 +0000
Subject: Exclude bot commands and seasonalbot commands from voice gate
---
postgres/init.sql | 7 +++++--
pydis_site/apps/api/models/bot/metricity.py | 20 +++++++++++++++++---
2 files changed, 22 insertions(+), 5 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/postgres/init.sql b/postgres/init.sql
index 40538492..c77fec9e 100644
--- a/postgres/init.sql
+++ b/postgres/init.sql
@@ -18,6 +18,7 @@ CREATE TABLE messages (
author_id varchar references users(id),
is_deleted boolean,
created_at timestamp,
+ channel_id varchar,
primary key(id)
);
@@ -25,12 +26,14 @@ INSERT INTO messages VALUES(
0,
0,
false,
- now()
+ now(),
+ '267659945086812160'
);
INSERT INTO messages VALUES(
1,
0,
false,
- now() + INTERVAL '10 minutes'
+ now() + INTERVAL '10 minutes,',
+ '1234'
);
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 379b0757..29d03d8b 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -2,6 +2,11 @@ from django.db import connections
BLOCK_INTERVAL = 10 * 60 # 10 minute blocks
+EXCLUDE_CHANNELS = [
+ "267659945086812160", # Bot commands
+ "607247579608121354" # SeasonalBot commands
+]
+
class NotFound(Exception):
"""Raised when an entity cannot be found."""
@@ -36,8 +41,16 @@ class Metricity:
def total_messages(self, user_id: str) -> int:
"""Query total number of messages for a user."""
self.cursor.execute(
- "SELECT COUNT(*) FROM messages WHERE author_id = '%s' AND NOT is_deleted",
- [user_id]
+ """
+ SELECT
+ COUNT(*)
+ FROM messages
+ WHERE
+ author_id = '%s'
+ AND NOT is_deleted
+ AND NOT %s::varchar[] @> ARRAY[channel_id]
+ """,
+ [user_id, EXCLUDE_CHANNELS]
)
values = self.cursor.fetchone()
@@ -63,10 +76,11 @@ class Metricity:
WHERE
author_id='%s'
AND NOT is_deleted
+ AND NOT %s::varchar[] @> ARRAY[channel_id]
GROUP BY interval
) block_query;
""",
- [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id]
+ [BLOCK_INTERVAL, BLOCK_INTERVAL, user_id, EXCLUDE_CHANNELS]
)
values = self.cursor.fetchone()
--
cgit v1.2.3
From 9efd0cb678e7d55db15272c17b996188fda4a211 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 5 Dec 2020 19:19:10 +0200
Subject: Include DestroyModelMixin to infractions view for DELETE method
Added this mixin and documented this in doctoring.
---
pydis_site/apps/api/viewsets/bot/infraction.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py
index edec0a1e..423e806e 100644
--- a/pydis_site/apps/api/viewsets/bot/infraction.py
+++ b/pydis_site/apps/api/viewsets/bot/infraction.py
@@ -5,6 +5,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.mixins import (
CreateModelMixin,
+ DestroyModelMixin,
ListModelMixin,
RetrieveModelMixin
)
@@ -18,7 +19,13 @@ from pydis_site.apps.api.serializers import (
)
-class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet):
+class InfractionViewSet(
+ CreateModelMixin,
+ RetrieveModelMixin,
+ ListModelMixin,
+ GenericViewSet,
+ DestroyModelMixin
+):
"""
View providing CRUD operations on infractions for Discord users.
@@ -108,6 +115,13 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
- 400: if a field in the request body is invalid or disallowed
- 404: if an infraction with the given `id` could not be found
+ ### DELETE /bot/infractions/
+ Delete the infraction with the given `id`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: if a infraction with the given `id` does not exist
+
### Expanded routes
All routes support expansion of `user` and `actor` in responses. To use an expanded route,
append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`.
--
cgit v1.2.3
From f5ab88b2f063b1d2d11eb48a421b8973e5fc6562 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 9 Dec 2020 08:18:03 +0200
Subject: Add tests for infraction deletion method
---
pydis_site/apps/api/tests/test_infractions.py | 30 +++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py
index 93ef8171..82b497aa 100644
--- a/pydis_site/apps/api/tests/test_infractions.py
+++ b/pydis_site/apps/api/tests/test_infractions.py
@@ -512,6 +512,36 @@ class CreationTests(APISubdomainTestCase):
)
+class InfractionDeletionTests(APISubdomainTestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.user = User.objects.create(
+ id=9876,
+ name='Unknown user',
+ discriminator=9876,
+ )
+
+ cls.warning = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='warning',
+ active=False
+ )
+
+ def test_delete_unknown_infraction_returns_404(self):
+ url = reverse('bot:infraction-detail', args=('something',), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 404)
+
+ def test_delete_known_infraction_returns_204(self):
+ url = reverse('bot:infraction-detail', args=(self.warning.id,), host='api')
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 204)
+ self.assertRaises(Infraction.DoesNotExist, Infraction.objects.get, id=self.warning.id)
+
+
class ExpandedTests(APISubdomainTestCase):
@classmethod
def setUpTestData(cls):
--
cgit v1.2.3
From e1008870619f92bc76ed61eb9adbc6ada795b23f Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Sat, 19 Dec 2020 03:40:25 +0000
Subject: Swap verified_at for joined_at
---
pydis_site/apps/api/models/bot/metricity.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py
index 29d03d8b..cae630f1 100644
--- a/pydis_site/apps/api/models/bot/metricity.py
+++ b/pydis_site/apps/api/models/bot/metricity.py
@@ -28,7 +28,8 @@ class Metricity:
def user(self, user_id: str) -> dict:
"""Query a user's data."""
- columns = ["verified_at"]
+ # TODO: Swap this back to some sort of verified at date
+ columns = ["joined_at"]
query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'"
self.cursor.execute(query, [user_id])
values = self.cursor.fetchone()
--
cgit v1.2.3
From eff717bb38e5b556751ba4f23a53cc6662baa7dd Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Sat, 19 Dec 2020 03:43:23 +0000
Subject: Update verified_at fields to joined_at fields
---
postgres/init.sql | 2 +-
pydis_site/apps/api/tests/test_users.py | 10 +++++-----
pydis_site/apps/api/viewsets/bot/user.py | 2 +-
3 files changed, 7 insertions(+), 7 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/postgres/init.sql b/postgres/init.sql
index c77fec9e..740063e7 100644
--- a/postgres/init.sql
+++ b/postgres/init.sql
@@ -4,7 +4,7 @@ CREATE DATABASE metricity;
CREATE TABLE users (
id varchar,
- verified_at timestamp,
+ joined_at timestamp,
primary key(id)
);
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
index c422f895..69bbfefc 100644
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -407,10 +407,10 @@ class UserMetricityTests(APISubdomainTestCase):
def test_get_metricity_data(self):
# Given
- verified_at = "foo"
+ joined_at = "foo"
total_messages = 1
total_blocks = 1
- self.mock_metricity_user(verified_at, total_messages, total_blocks)
+ self.mock_metricity_user(joined_at, total_messages, total_blocks)
# When
url = reverse('bot:user-metricity-data', args=[0], host='api')
@@ -419,7 +419,7 @@ class UserMetricityTests(APISubdomainTestCase):
# Then
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {
- "verified_at": verified_at,
+ "joined_at": joined_at,
"total_messages": total_messages,
"voice_banned": False,
"activity_blocks": total_blocks
@@ -455,12 +455,12 @@ class UserMetricityTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["voice_banned"], case["voice_banned"])
- def mock_metricity_user(self, verified_at, total_messages, total_blocks):
+ def mock_metricity_user(self, joined_at, total_messages, total_blocks):
patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity")
self.metricity = patcher.start()
self.addCleanup(patcher.stop)
self.metricity = self.metricity.return_value.__enter__.return_value
- self.metricity.user.return_value = dict(verified_at=verified_at)
+ self.metricity.user.return_value = dict(joined_at=joined_at)
self.metricity.total_messages.return_value = total_messages
self.metricity.total_message_blocks.return_value = total_blocks
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 79f90163..829e2694 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -109,7 +109,7 @@ class UserViewSet(ModelViewSet):
#### Response format
>>> {
- ... "verified_at": "2020-10-06T21:54:23.540766",
+ ... "joined_at": "2020-10-06T21:54:23.540766",
... "total_messages": 2,
... "voice_banned": False,
... "activity_blocks": 1
--
cgit v1.2.3
From 9ac477385d5ed26d2d8e4f711b2c927cfaf35461 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 08:21:18 +0200
Subject: Split nomination model to 2 tables and create migrations
---
.../api/migrations/0068_split_nomination_tables.py | 60 ++++++++++++++++++++++
.../0069_change_nomination_entry_plural.py | 17 ++++++
pydis_site/apps/api/models/bot/nomination.py | 47 ++++++++++++-----
3 files changed, 112 insertions(+), 12 deletions(-)
create mode 100644 pydis_site/apps/api/migrations/0068_split_nomination_tables.py
create mode 100644 pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
new file mode 100644
index 00000000..2e2313ee
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
@@ -0,0 +1,60 @@
+# Generated by Django 3.0.11 on 2021-02-21 15:32
+
+from django.apps.registry import Apps
+from django.db import backends, migrations, models
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+import django.db.models.deletion
+import pydis_site.apps.api.models.mixins
+
+
+def migrate_nominations(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
+ Nomination = apps.get_model("api", "Nomination")
+ NominationEntry = apps.get_model("api", "NominationEntry")
+
+ for nomination in Nomination.objects.all():
+ nomination_entry = NominationEntry(
+ nomination=nomination,
+ actor=nomination.actor,
+ reason=nomination.reason,
+ inserted_at=nomination.inserted_at
+ )
+ nomination_entry.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0067_add_voice_ban_infraction_type'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='NominationEntry',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('reason', models.TextField(blank=True, help_text='Why the actor nominated this user.', null=True)),
+ ('inserted_at',
+ models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination entry.')),
+ ('actor', models.ForeignKey(help_text='The staff member that nominated this user.',
+ on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set',
+ to='api.User')),
+ ('nomination', models.ForeignKey(help_text='Nomination to what this entry belongs.',
+ on_delete=django.db.models.deletion.CASCADE, to='api.Nomination')),
+ ],
+ bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
+ ),
+ migrations.RunPython(migrate_nominations),
+ migrations.RemoveField(
+ model_name='nomination',
+ name='actor',
+ ),
+ migrations.RemoveField(
+ model_name='nomination',
+ name='reason',
+ ),
+ migrations.AddField(
+ model_name='nomination',
+ name='reviewed',
+ field=models.BooleanField(default=False, help_text='Whether voting message have been made.'),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py b/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py
new file mode 100644
index 00000000..6bf4ac8c
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.0.11 on 2021-02-21 16:44
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0068_split_nomination_tables'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='nominationentry',
+ options={'verbose_name_plural': 'nomination entries'},
+ ),
+ ]
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index 11b9e36e..ed6f7d81 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -5,23 +5,12 @@ from pydis_site.apps.api.models.mixins import ModelReprMixin
class Nomination(ModelReprMixin, models.Model):
- """A helper nomination created by staff."""
+ """A general helper nomination information created by staff."""
active = models.BooleanField(
default=True,
help_text="Whether this nomination is still relevant."
)
- actor = models.ForeignKey(
- User,
- on_delete=models.CASCADE,
- help_text="The staff member that nominated this user.",
- related_name='nomination_set'
- )
- reason = models.TextField(
- help_text="Why this user was nominated.",
- null=True,
- blank=True
- )
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
@@ -42,6 +31,10 @@ class Nomination(ModelReprMixin, models.Model):
help_text="When the nomination was ended.",
null=True
)
+ reviewed = models.BooleanField(
+ default=False,
+ help_text="Whether voting message have been made."
+ )
def __str__(self):
"""Representation that makes the target and state of the nomination immediately evident."""
@@ -52,3 +45,33 @@ class Nomination(ModelReprMixin, models.Model):
"""Set the ordering of nominations to most recent first."""
ordering = ("-inserted_at",)
+
+
+class NominationEntry(ModelReprMixin, models.Model):
+ """A nomination entry created by single staff."""
+
+ nomination = models.ForeignKey(
+ Nomination,
+ on_delete=models.CASCADE,
+ help_text="Nomination to what this entry belongs."
+ )
+ actor = models.ForeignKey(
+ User,
+ on_delete=models.CASCADE,
+ help_text="The staff member that nominated this user.",
+ related_name='nomination_set'
+ )
+ reason = models.TextField(
+ help_text="Why the actor nominated this user.",
+ null=True,
+ blank=True
+ )
+ inserted_at = models.DateTimeField(
+ auto_now_add=True,
+ help_text="The creation date of this nomination entry."
+ )
+
+ class Meta:
+ """Meta options for NominationEntry model."""
+
+ verbose_name_plural = "nomination entries"
--
cgit v1.2.3
From ff46d9f59b5da3aff5b8efbca3214ee0d3c064c1 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 08:21:54 +0200
Subject: Change nominations admin interface and add nomination entries
interface
---
pydis_site/apps/api/admin.py | 80 +++++++++++++++++++++++++++++++++++++-------
1 file changed, 67 insertions(+), 13 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py
index b6fee9d1..449e660e 100644
--- a/pydis_site/apps/api/admin.py
+++ b/pydis_site/apps/api/admin.py
@@ -21,6 +21,7 @@ from .models import (
Role,
User
)
+from .models.bot.nomination import NominationEntry
admin.site.site_header = "Python Discord | Administration"
admin.site.site_title = "Python Discord"
@@ -218,7 +219,7 @@ class NominationActorFilter(admin.SimpleListFilter):
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()
+ actor_ids = NominationEntry.objects.order_by().values_list("actor").distinct()
actors = User.objects.filter(id__in=actor_ids)
return ((a.id, a.username) for a in actors)
@@ -226,7 +227,10 @@ class NominationActorFilter(admin.SimpleListFilter):
"""Query to filter the list of Users against."""
if not self.value():
return
- return queryset.filter(actor__id=self.value())
+ nomination_ids = NominationEntry.objects.filter(
+ actor__id=self.value()
+ ).values_list("nomination_id").distinct()
+ return queryset.filter(id__in=nomination_ids)
@admin.register(Nomination)
@@ -236,9 +240,6 @@ class NominationAdmin(admin.ModelAdmin):
search_fields = (
"user__name",
"user__id",
- "actor__name",
- "actor__id",
- "reason",
"end_reason"
)
@@ -247,27 +248,25 @@ class NominationAdmin(admin.ModelAdmin):
list_display = (
"user",
"active",
- "reason",
- "actor",
+ "reviewed"
)
fields = (
"user",
"active",
- "actor",
- "reason",
"inserted_at",
"ended_at",
- "end_reason"
+ "end_reason",
+ "reviewed"
)
- # only allow reason fields to be edited.
+ # only allow end reason field to be edited.
readonly_fields = (
"user",
"active",
- "actor",
"inserted_at",
- "ended_at"
+ "ended_at",
+ "reviewed"
)
def has_add_permission(self, *args) -> bool:
@@ -275,6 +274,61 @@ class NominationAdmin(admin.ModelAdmin):
return False
+class NominationEntryActorFilter(admin.SimpleListFilter):
+ """Actor Filter for NominationEntry 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 = NominationEntry.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(NominationEntry)
+class NominationEntryAdmin(admin.ModelAdmin):
+ """Admin formatting for the NominationEntry model."""
+
+ search_fields = (
+ "actor__name",
+ "actor__id",
+ "reason",
+ )
+
+ list_filter = (NominationEntryActorFilter,)
+
+ list_display = (
+ "nomination",
+ "actor",
+ )
+
+ fields = (
+ "nomination",
+ "actor",
+ "reason",
+ "inserted_at",
+ )
+
+ # only allow reason field to be edited
+ readonly_fields = (
+ "nomination",
+ "actor",
+ "inserted_at",
+ )
+
+ def has_add_permission(self, request: HttpRequest) -> bool:
+ """Disable adding new nomination entry from admin."""
+ return False
+
+
@admin.register(OffTopicChannelName)
class OffTopicChannelNameAdmin(admin.ModelAdmin):
"""Admin formatting for the OffTopicChannelName model."""
--
cgit v1.2.3
From 4bc55a5e606de8037d52e992dd308f52c86a8e5f Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 08:22:15 +0200
Subject: Change nominations serializer and add nomination entry serializer
---
pydis_site/apps/api/serializers.py | 20 ++++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 10eb3839..65c69849 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -26,6 +26,7 @@ from .models import (
Role,
User
)
+from .models.bot.nomination import NominationEntry
class BotSettingSerializer(ModelSerializer):
@@ -338,6 +339,21 @@ class UserSerializer(ModelSerializer):
raise ValidationError({"id": ["User with ID already present."]})
+class NominationEntrySerializer(ModelSerializer):
+ """A class providing (de-)serialization of `NominationEntry` instances."""
+
+ nomination = PrimaryKeyRelatedField(
+ queryset=Nomination.objects.all(),
+ write_only=True
+ )
+
+ class Meta:
+ """Metadata defined for the Django REST framework."""
+
+ model = NominationEntry
+ fields = ('nomination', 'actor', 'reason', 'inserted_at')
+
+
class NominationSerializer(ModelSerializer):
"""A class providing (de-)serialization of `Nomination` instances."""
@@ -346,8 +362,8 @@ class NominationSerializer(ModelSerializer):
model = Nomination
fields = (
- 'id', 'active', 'actor', 'reason', 'user',
- 'inserted_at', 'end_reason', 'ended_at')
+ 'id', 'active', 'user', 'inserted_at', 'end_reason', 'ended_at'
+ )
class OffensiveMessageSerializer(ModelSerializer):
--
cgit v1.2.3
From 51c57d3707c684bb908195f419e53f4ed164ba3b Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 08:23:04 +0200
Subject: Update nominations viewset GET and POST to make this working with
2-table system
---
pydis_site/apps/api/viewsets/bot/nomination.py | 119 ++++++++++++++++++++-----
1 file changed, 96 insertions(+), 23 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index cf6e262f..8775515c 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -15,7 +15,8 @@ from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from pydis_site.apps.api.models.bot import Nomination
-from pydis_site.apps.api.serializers import NominationSerializer
+from pydis_site.apps.api.models.bot.nomination import NominationEntry
+from pydis_site.apps.api.serializers import NominationEntrySerializer, NominationSerializer
class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet):
@@ -29,7 +30,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
#### Query parameters
- **active** `bool`: whether the nomination is still active
- - **actor__id** `int`: snowflake of the user who nominated the user
- **user__id** `int`: snowflake of the user who received the nomination
- **ordering** `str`: comma-separated sequence of fields to order the returned results
@@ -40,12 +40,18 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
... {
... 'id': 1,
... 'active': false,
- ... 'actor': 336843820513755157,
- ... 'reason': 'They know how to explain difficult concepts',
... 'user': 336843820513755157,
... 'inserted_at': '2019-04-25T14:02:37.775587Z',
... 'end_reason': 'They were helpered after a staff-vote',
- ... 'ended_at': '2019-04-26T15:12:22.123587Z'
+ ... 'ended_at': '2019-04-26T15:12:22.123587Z',
+ ... 'entries': [
+ ... {
+ ... 'actor': 336843820513755157,
+ ... 'reason': 'They know how to explain difficult concepts',
+ ... 'inserted_at': '2019-04-25T14:02:37.775587Z'
+ ... }
+ ... ],
+ ... 'reviewed': true
... }
... ]
@@ -59,12 +65,18 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
>>> {
... 'id': 1,
... 'active': true,
- ... 'actor': 336843820513755157,
- ... 'reason': 'They know how to explain difficult concepts',
... 'user': 336843820513755157,
... 'inserted_at': '2019-04-25T14:02:37.775587Z',
... 'end_reason': 'They were helpered after a staff-vote',
- ... 'ended_at': '2019-04-26T15:12:22.123587Z'
+ ... 'ended_at': '2019-04-26T15:12:22.123587Z',
+ ... 'entries': [
+ ... {
+ ... 'actor': 336843820513755157,
+ ... 'reason': 'They know how to explain difficult concepts',
+ ... 'inserted_at': '2019-04-25T14:02:37.775587Z'
+ ... }
+ ... ],
+ ... 'reviewed': false
... }
### Status codes
@@ -75,8 +87,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
Create a new, active nomination returns the created nominations.
The `user`, `reason` and `actor` fields are required and the `user`
and `actor` need to know by the site. Providing other valid fields
- is not allowed and invalid fields are ignored. A `user` is only
- allowed one active nomination at a time.
+ is not allowed and invalid fields are ignored. If `user` already have
+ active nomination, new nomination entry will be created assigned to
+ active nomination.
#### Request body
>>> {
@@ -91,7 +104,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
#### Status codes
- 201: returned on success
- 400: returned on failure for one of the following reasons:
- - A user already has an active nomination;
- The `user` or `actor` are unknown to the site;
- The request contained a field that cannot be set at creation.
@@ -148,10 +160,44 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
serializer_class = NominationSerializer
queryset = Nomination.objects.all()
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
- filter_fields = ('user__id', 'actor__id', 'active')
- frozen_fields = ('id', 'actor', 'inserted_at', 'user', 'ended_at')
+ filter_fields = ('user__id', 'active')
+ frozen_fields = ('id', 'inserted_at', 'user', 'ended_at')
frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at')
+ def list(self, request: HttpRequest, *args, **kwargs) -> Response:
+ """
+ DRF method for listing Nominations.
+
+ Called by the Django Rest Framework in response to the corresponding HTTP request.
+ """
+ queryset = self.filter_queryset(self.get_queryset())
+ data = NominationSerializer(queryset, many=True).data
+
+ for i, nomination in enumerate(data):
+ entries = NominationEntrySerializer(
+ NominationEntry.objects.filter(nomination_id=nomination["id"]),
+ many=True
+ ).data
+ data[i]["entries"] = entries
+
+ return Response(data)
+
+ def retrieve(self, request: HttpRequest, *args, **kwargs) -> Response:
+ """
+ DRF method for retrieving a Nomination.
+
+ Called by the Django Rest Framework in response to the corresponding HTTP request.
+ """
+ nomination = self.get_object()
+
+ data = NominationSerializer(nomination).data
+ data["entries"] = NominationEntrySerializer(
+ NominationEntry.objects.filter(nomination_id=nomination.id),
+ many=True
+ ).data
+
+ return Response(data)
+
def create(self, request: HttpRequest, *args, **kwargs) -> Response:
"""
DRF method for creating a Nomination.
@@ -163,19 +209,46 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
raise ValidationError({field: ['This field cannot be set at creation.']})
user_id = request.data.get("user")
- if Nomination.objects.filter(active=True, user__id=user_id).exists():
- raise ValidationError({'active': ['There can only be one active nomination.']})
+ nomination_filter = Nomination.objects.filter(active=True, user__id=user_id)
+
+ if not nomination_filter.exists():
+ serializer = NominationSerializer(
+ data=ChainMap(
+ request.data,
+ {"active": True}
+ )
+ )
+ serializer.is_valid(raise_exception=True)
+ nomination = Nomination.objects.create(**serializer.validated_data)
- serializer = self.get_serializer(
- data=ChainMap(
- request.data,
- {"active": True}
+ # Serializer truncate unnecessary data away
+ entry_serializer = NominationEntrySerializer(
+ data=ChainMap(request.data, {"nomination": nomination.id})
)
+ entry_serializer.is_valid(raise_exception=True)
+
+ entry = NominationEntry.objects.create(**entry_serializer.validated_data)
+
+ data = NominationSerializer(nomination).data
+ data["entries"] = NominationEntrySerializer([entry], many=True).data
+
+ headers = self.get_success_headers(data)
+ return Response(data, status=status.HTTP_201_CREATED, headers=headers)
+
+ entry_serializer = NominationEntrySerializer(
+ data=ChainMap(request.data, {"nomination": nomination_filter[0].id})
)
- serializer.is_valid(raise_exception=True)
- self.perform_create(serializer)
- headers = self.get_success_headers(serializer.data)
- return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+ entry_serializer.is_valid(raise_exception=True)
+ NominationEntry.objects.create(**entry_serializer.validated_data)
+
+ data = NominationSerializer(nomination_filter[0]).data
+ data["entries"] = NominationEntrySerializer(
+ NominationEntry.objects.filter(nomination_id=nomination_filter[0].id),
+ many=True
+ ).data
+
+ headers = self.get_success_headers(data)
+ return Response(data, status=status.HTTP_201_CREATED, headers=headers)
def partial_update(self, request: HttpRequest, *args, **kwargs) -> Response:
"""
--
cgit v1.2.3
From 71388fff77dcb2b074e02dfe33133d779c9afa2d Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 09:00:54 +0200
Subject: Add reviewed field to nomination serializer
---
pydis_site/apps/api/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 65c69849..96f28aee 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -362,7 +362,7 @@ class NominationSerializer(ModelSerializer):
model = Nomination
fields = (
- 'id', 'active', 'user', 'inserted_at', 'end_reason', 'ended_at'
+ 'id', 'active', 'user', 'inserted_at', 'end_reason', 'ended_at', 'reviewed'
)
--
cgit v1.2.3
From e17da1f2e991710beaabd37701ce06a57f7b4a77 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 09:01:18 +0200
Subject: Migrate PATCH request for 2-table nominations system
---
pydis_site/apps/api/viewsets/bot/nomination.py | 70 ++++++++++++++++++++++----
1 file changed, 60 insertions(+), 10 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index 8775515c..14dee9bc 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -112,18 +112,20 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
The PATCH route can be used for three distinct operations:
1. Updating the `reason` of `active` nomination;
- 2. Ending an `active` nomination;
- 3. Updating the `end_reason` or `reason` field of an `inactive` nomination.
+ 2. Updating `reviewed` field of `active` nomination.
+ 3. Ending an `active` nomination;
+ 4. Updating the `end_reason` or `reason` field of an `inactive` nomination.
While the response format and status codes are the same for all three operations (see
below), the request bodies vary depending on the operation. For all operations it holds
that providing other valid fields is not allowed and invalid fields are ignored.
- ### 1. Updating the `reason` of `active` nomination
+ ### 1. Updating the `reason` of `active` nomination. Actor field is required.
#### Request body
>>> {
... 'reason': 'He would make a great helper',
+ ... 'actor': 409107086526644234
... }
#### Response format
@@ -134,7 +136,16 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
- 400: if a field in the request body is invalid or disallowed
- 404: if an infraction with the given `id` could not be found
- ### 2. Ending an `active` nomination
+ ### 2. Setting nomination `reviewed`
+
+ #### Request body
+ >>> {
+ ... 'reviewed': True
+ ... }
+
+ See operation 1 for the response format and status codes.
+
+ ### 3. Ending an `active` nomination
#### Request body
>>> {
@@ -144,11 +155,13 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
See operation 1 for the response format and status codes.
- ### 3. Updating the `end_reason` or `reason` field of an `inactive` nomination.
+ ### 4. Updating the `end_reason` or `reason` field of an `inactive` nomination.
+ Actor field is required when updating reason.
#### Request body
>>> {
... 'reason': 'Updated reason for this nomination',
+ ... 'actor': 409107086526644234,
... 'end_reason': 'Updated end_reason for this nomination',
... }
@@ -162,7 +175,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
filter_fields = ('user__id', 'active')
frozen_fields = ('id', 'inserted_at', 'user', 'ended_at')
- frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at')
+ frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at', 'reviewed')
def list(self, request: HttpRequest, *args, **kwargs) -> Response:
"""
@@ -274,8 +287,20 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
{'end_reason': ["An active nomination can't have an end reason."]}
)
+ elif 'reviewed' in data:
+ # 2. We're setting nomination reviewed
+ if not instance.active:
+ raise ValidationError(
+ {'reviewed': 'This field cannot be set if nomination is inactive.'}
+ )
+
+ if 'active' in data:
+ raise ValidationError(
+ {'active': 'This field cannot be set same time than ending nomination.'}
+ )
+
elif instance.active and not data['active']:
- # 2. We're ending an active nomination.
+ # 3. We're ending an active nomination.
if 'reason' in data:
raise ValidationError(
{'reason': ['This field cannot be set when ending a nomination.']}
@@ -289,11 +314,36 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
instance.ended_at = timezone.now()
elif 'active' in data:
- # 3. The `active` field is only allowed when ending a nomination.
+ # 4. The `active` field is only allowed when ending a nomination.
raise ValidationError(
{'active': ['This field can only be used to end a nomination']}
)
- serializer.save()
+ if 'reason' in request.data:
+ if 'actor' not in request.data:
+ raise ValidationError(
+ {'actor': 'This field is required when editing reason.'}
+ )
+
+ entry_filter = NominationEntry.objects.filter(
+ nomination_id=instance.id,
+ actor__id=request.data['actor']
+ )
+
+ if not entry_filter.exists():
+ raise ValidationError(
+ {'actor': "Actor don't exist or have not nominated user."}
+ )
+
+ entry = entry_filter[0]
+ entry.reason = request.data['reason']
+ entry.save()
+
+ nomination = serializer.save()
+ return_data = NominationSerializer(nomination).data
+ return_data["entries"] = NominationEntrySerializer(
+ NominationEntry.objects.filter(nomination_id=nomination.id),
+ many=True
+ ).data
- return Response(serializer.data)
+ return Response(return_data)
--
cgit v1.2.3
From 0ae69d9892db73029336e7b9ca95164dd80f828f Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 09:05:54 +0200
Subject: Disable creating multiple nomination entries of one nomination for
one actor
---
pydis_site/apps/api/viewsets/bot/nomination.py | 10 ++++++++++
1 file changed, 10 insertions(+)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index 14dee9bc..81fb43f7 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -252,6 +252,16 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
data=ChainMap(request.data, {"nomination": nomination_filter[0].id})
)
entry_serializer.is_valid(raise_exception=True)
+
+ # Don't allow user creating many nomination entries for one nomination
+ if NominationEntry.objects.filter(
+ nomination_id=nomination_filter[0].id,
+ actor__id=entry_serializer.validated_data["actor"].id
+ ).exists():
+ raise ValidationError(
+ {'actor': 'This actor have already created nomination entry for this nomination.'}
+ )
+
NominationEntry.objects.create(**entry_serializer.validated_data)
data = NominationSerializer(nomination_filter[0]).data
--
cgit v1.2.3
From 2fb5bf5dbdc6cdfe0a62d54bc7b726eab0199a59 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 09:36:55 +0200
Subject: Add rollback to nominations table split migration
---
.../apps/api/migrations/0068_split_nomination_tables.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
index 2e2313ee..1c392365 100644
--- a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
+++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
@@ -21,6 +21,19 @@ def migrate_nominations(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) ->
nomination_entry.save()
+def unmigrate_nominations(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
+ Nomination = apps.get_model("api", "Nomination")
+ NominationEntry = apps.get_model("api", "NominationEntry")
+
+ for entry in NominationEntry.objects.all():
+ nomination = Nomination.objects.get(pk=entry.nomination.id)
+ nomination.actor = entry.actor
+ nomination.reason = entry.reason
+ nomination.inserted_at = entry.inserted_at
+
+ nomination.save()
+
+
class Migration(migrations.Migration):
dependencies = [
@@ -43,7 +56,7 @@ class Migration(migrations.Migration):
],
bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
),
- migrations.RunPython(migrate_nominations),
+ migrations.RunPython(migrate_nominations, unmigrate_nominations),
migrations.RemoveField(
model_name='nomination',
name='actor',
--
cgit v1.2.3
From 698cbc17405a49fe42669fedd6054ab6c9008a4a Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 09:37:12 +0200
Subject: Wrap validation errors to []
---
pydis_site/apps/api/viewsets/bot/nomination.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index 81fb43f7..c4600425 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -259,7 +259,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
actor__id=entry_serializer.validated_data["actor"].id
).exists():
raise ValidationError(
- {'actor': 'This actor have already created nomination entry for this nomination.'}
+ {'actor': ['This actor have already created nomination entry for this nomination.']}
)
NominationEntry.objects.create(**entry_serializer.validated_data)
@@ -301,12 +301,12 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
# 2. We're setting nomination reviewed
if not instance.active:
raise ValidationError(
- {'reviewed': 'This field cannot be set if nomination is inactive.'}
+ {'reviewed': ['This field cannot be set if nomination is inactive.']}
)
if 'active' in data:
raise ValidationError(
- {'active': 'This field cannot be set same time than ending nomination.'}
+ {'active': ['This field cannot be set same time than ending nomination.']}
)
elif instance.active and not data['active']:
@@ -332,7 +332,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
if 'reason' in request.data:
if 'actor' not in request.data:
raise ValidationError(
- {'actor': 'This field is required when editing reason.'}
+ {'actor': ['This field is required when editing reason.']}
)
entry_filter = NominationEntry.objects.filter(
@@ -342,7 +342,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
if not entry_filter.exists():
raise ValidationError(
- {'actor': "Actor don't exist or have not nominated user."}
+ {'actor': ["Actor don't exist or have not nominated user."]}
)
entry = entry_filter[0]
--
cgit v1.2.3
From 6591270f832e93a3eaf941aa1aeeddb6af7bce36 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 11:36:25 +0200
Subject: Import NominationEntry to models __init__.py
---
pydis_site/apps/api/models/__init__.py | 1 +
pydis_site/apps/api/models/bot/__init__.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py
index 0a8c90f6..fd5bf220 100644
--- a/pydis_site/apps/api/models/__init__.py
+++ b/pydis_site/apps/api/models/__init__.py
@@ -8,6 +8,7 @@ from .bot import (
Message,
MessageDeletionContext,
Nomination,
+ NominationEntry,
OffensiveMessage,
OffTopicChannelName,
Reminder,
diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py
index 1673b434..ac864de3 100644
--- a/pydis_site/apps/api/models/bot/__init__.py
+++ b/pydis_site/apps/api/models/bot/__init__.py
@@ -6,7 +6,7 @@ from .documentation_link import DocumentationLink
from .infraction import Infraction
from .message import Message
from .message_deletion_context import MessageDeletionContext
-from .nomination import Nomination
+from .nomination import Nomination, NominationEntry
from .off_topic_channel_name import OffTopicChannelName
from .offensive_message import OffensiveMessage
from .reminder import Reminder
--
cgit v1.2.3
From 126b5084f1f583790bcd1a386474230012d7cadb Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 11:36:47 +0200
Subject: Change NominationEntry import location in serializers
---
pydis_site/apps/api/serializers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 96f28aee..49c247b5 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -20,13 +20,13 @@ from .models import (
Infraction,
MessageDeletionContext,
Nomination,
+ NominationEntry,
OffTopicChannelName,
OffensiveMessage,
Reminder,
Role,
User
)
-from .models.bot.nomination import NominationEntry
class BotSettingSerializer(ModelSerializer):
--
cgit v1.2.3
From 3907122c9e76f78bd7bfd07028e9eb79b43d65b3 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 11:37:33 +0200
Subject: Small improvements in nomination viewset
---
pydis_site/apps/api/viewsets/bot/nomination.py | 62 +++++++++++++-------------
1 file changed, 31 insertions(+), 31 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index c4600425..7820ca0d 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -14,8 +14,7 @@ from rest_framework.mixins import (
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
-from pydis_site.apps.api.models.bot import Nomination
-from pydis_site.apps.api.models.bot.nomination import NominationEntry
+from pydis_site.apps.api.models.bot import Nomination, NominationEntry
from pydis_site.apps.api.serializers import NominationEntrySerializer, NominationSerializer
@@ -112,9 +111,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
The PATCH route can be used for three distinct operations:
1. Updating the `reason` of `active` nomination;
- 2. Updating `reviewed` field of `active` nomination.
- 3. Ending an `active` nomination;
- 4. Updating the `end_reason` or `reason` field of an `inactive` nomination.
+ 2. Ending an `active` nomination;
+ 3. Updating the `end_reason` or `reason` field of an `inactive` nomination.
+ 4. Updating `reviewed` field of `active` nomination.
While the response format and status codes are the same for all three operations (see
below), the request bodies vary depending on the operation. For all operations it holds
@@ -136,16 +135,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
- 400: if a field in the request body is invalid or disallowed
- 404: if an infraction with the given `id` could not be found
- ### 2. Setting nomination `reviewed`
-
- #### Request body
- >>> {
- ... 'reviewed': True
- ... }
-
- See operation 1 for the response format and status codes.
-
- ### 3. Ending an `active` nomination
+ ### 2. Ending an `active` nomination
#### Request body
>>> {
@@ -155,7 +145,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
See operation 1 for the response format and status codes.
- ### 4. Updating the `end_reason` or `reason` field of an `inactive` nomination.
+ ### 3. Updating the `end_reason` or `reason` field of an `inactive` nomination.
Actor field is required when updating reason.
#### Request body
@@ -167,6 +157,15 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
Note: The request body may contain either or both fields.
+ See operation 1 for the response format and status codes.
+
+ ### 4. Setting nomination `reviewed`
+
+ #### Request body
+ >>> {
+ ... 'reviewed': True
+ ... }
+
See operation 1 for the response format and status codes.
"""
@@ -297,21 +296,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
{'end_reason': ["An active nomination can't have an end reason."]}
)
- elif 'reviewed' in data:
- # 2. We're setting nomination reviewed
- if not instance.active:
- raise ValidationError(
- {'reviewed': ['This field cannot be set if nomination is inactive.']}
- )
-
- if 'active' in data:
- raise ValidationError(
- {'active': ['This field cannot be set same time than ending nomination.']}
- )
-
elif instance.active and not data['active']:
- # 3. We're ending an active nomination.
- if 'reason' in data:
+ # 2. We're ending an active nomination.
+ if 'reason' in request.data:
raise ValidationError(
{'reason': ['This field cannot be set when ending a nomination.']}
)
@@ -321,14 +308,27 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
{'end_reason': ['This field is required when ending a nomination.']}
)
+ if 'reviewed' in request.data:
+ raise ValidationError(
+ {'reviewed': ['This field cannot be set same time than ending nomination.']}
+ )
+
instance.ended_at = timezone.now()
elif 'active' in data:
- # 4. The `active` field is only allowed when ending a nomination.
+ # 3. The `active` field is only allowed when ending a nomination.
raise ValidationError(
{'active': ['This field can only be used to end a nomination']}
)
+ # This is actually covered, but for some reason coverage don't think so.
+ elif 'reviewed' in request.data: # pragma: no cover
+ # 4. We're setting nomination reviewed
+ if not instance.active:
+ raise ValidationError(
+ {'reviewed': ['This field cannot be set if nomination is inactive.']}
+ )
+
if 'reason' in request.data:
if 'actor' not in request.data:
raise ValidationError(
--
cgit v1.2.3
From 9af0579297e8f1b535099f24f41b4c7f85e2a0cc Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Mon, 22 Feb 2021 11:37:53 +0200
Subject: Update nomination-related tests to cover recent table-splitting
changes
---
pydis_site/apps/api/tests/test_models.py | 16 +--
pydis_site/apps/api/tests/test_nominations.py | 144 +++++++++++++++++++++++---
2 files changed, 139 insertions(+), 21 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py
index 853e6621..66052e01 100644
--- a/pydis_site/apps/api/tests/test_models.py
+++ b/pydis_site/apps/api/tests/test_models.py
@@ -10,6 +10,7 @@ from pydis_site.apps.api.models import (
Message,
MessageDeletionContext,
Nomination,
+ NominationEntry,
OffTopicChannelName,
OffensiveMessage,
Reminder,
@@ -37,17 +38,11 @@ class StringDunderMethodTests(SimpleTestCase):
def setUp(self):
self.nomination = Nomination(
id=123,
- actor=User(
- id=9876,
- name='Mr. Hemlock',
- discriminator=6666,
- ),
user=User(
id=9876,
name="Hemlock's Cat",
discriminator=7777,
),
- reason="He purrrrs like the best!",
)
self.objects = (
@@ -135,6 +130,15 @@ class StringDunderMethodTests(SimpleTestCase):
),
content="oh no",
expiration=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc)
+ ),
+ NominationEntry(
+ nomination_id=self.nomination.id,
+ actor=User(
+ id=9876,
+ name='Mr. Hemlock',
+ discriminator=6666,
+ ),
+ reason="He purrrrs like the best!",
)
)
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
index b37135f8..3892ec6e 100644
--- a/pydis_site/apps/api/tests/test_nominations.py
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -3,7 +3,7 @@ from datetime import datetime as dt, timedelta, timezone
from django_hosts.resolvers import reverse
from .base import APISubdomainTestCase
-from ..models import Nomination, User
+from ..models import Nomination, NominationEntry, User
class CreationTests(APISubdomainTestCase):
@@ -14,6 +14,11 @@ class CreationTests(APISubdomainTestCase):
name='joe dart',
discriminator=1111,
)
+ cls.user2 = User.objects.create(
+ id=9876,
+ name='Who?',
+ discriminator=1234
+ )
def test_accepts_valid_data(self):
url = reverse('bot:nomination-list', host='api')
@@ -27,17 +32,39 @@ class CreationTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 201)
nomination = Nomination.objects.get(id=response.json()['id'])
+ nomination_entry = NominationEntry.objects.get(
+ nomination_id=nomination.id,
+ actor_id=self.user.id
+ )
self.assertAlmostEqual(
nomination.inserted_at,
dt.now(timezone.utc),
delta=timedelta(seconds=2)
)
self.assertEqual(nomination.user.id, data['user'])
- self.assertEqual(nomination.actor.id, data['actor'])
- self.assertEqual(nomination.reason, data['reason'])
+ self.assertEqual(nomination_entry.reason, data['reason'])
self.assertEqual(nomination.active, True)
- def test_returns_400_on_second_active_nomination(self):
+ def test_returns_200_on_second_active_nomination_by_different_user(self):
+ url = reverse('bot:nomination-list', host='api')
+ first_data = {
+ 'actor': self.user.id,
+ 'reason': 'Joe Dart on Fender Bass',
+ 'user': self.user.id,
+ }
+ second_data = {
+ 'actor': self.user2.id,
+ 'reason': 'Great user',
+ 'user': self.user.id
+ }
+
+ response1 = self.client.post(url, data=first_data)
+ self.assertEqual(response1.status_code, 201)
+
+ response2 = self.client.post(url, data=second_data)
+ self.assertEqual(response2.status_code, 201)
+
+ def test_returns_400_on_second_active_nomination_by_existing_nominator(self):
url = reverse('bot:nomination-list', host='api')
data = {
'actor': self.user.id,
@@ -51,7 +78,7 @@ class CreationTests(APISubdomainTestCase):
response2 = self.client.post(url, data=data)
self.assertEqual(response2.status_code, 400)
self.assertEqual(response2.json(), {
- 'active': ['There can only be one active nomination.']
+ 'actor': ['This actor have already created nomination entry for this nomination.']
})
def test_returns_400_for_missing_user(self):
@@ -189,30 +216,40 @@ class NominationTests(APISubdomainTestCase):
)
cls.active_nomination = Nomination.objects.create(
- user=cls.user,
+ user=cls.user
+ )
+ cls.active_nomination_entry = NominationEntry.objects.create(
+ nomination=cls.active_nomination,
actor=cls.user,
reason="He's pretty funky"
)
cls.inactive_nomination = Nomination.objects.create(
user=cls.user,
- actor=cls.user,
- reason="He's pretty funky",
active=False,
end_reason="His neck couldn't hold the funk",
ended_at="5018-11-20T15:52:00+00:00"
)
+ cls.inactive_nomination_entry = NominationEntry.objects.create(
+ nomination=cls.inactive_nomination,
+ actor=cls.user,
+ reason="He's pretty funky"
+ )
- def test_returns_200_update_reason_on_active(self):
+ def test_returns_200_update_reason_on_active_with_actor(self):
url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api')
data = {
- 'reason': "He's one funky duck"
+ 'reason': "He's one funky duck",
+ 'actor': self.user.id
}
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 200)
- nomination = Nomination.objects.get(id=response.json()['id'])
- self.assertEqual(nomination.reason, data['reason'])
+ nomination_entry = NominationEntry.objects.get(
+ nomination_id=response.json()['id'],
+ actor_id=self.user.id
+ )
+ self.assertEqual(nomination_entry.reason, data['reason'])
def test_returns_400_on_frozen_field_update(self):
url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api')
@@ -241,14 +278,18 @@ class NominationTests(APISubdomainTestCase):
def test_returns_200_update_reason_on_inactive(self):
url = reverse('bot:nomination-detail', args=(self.inactive_nomination.id,), host='api')
data = {
- 'reason': "He's one funky duck"
+ 'reason': "He's one funky duck",
+ 'actor': self.user.id
}
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 200)
- nomination = Nomination.objects.get(id=response.json()['id'])
- self.assertEqual(nomination.reason, data['reason'])
+ nomination_entry = NominationEntry.objects.get(
+ nomination_id=response.json()['id'],
+ actor_id=self.user.id
+ )
+ self.assertEqual(nomination_entry.reason, data['reason'])
def test_returns_200_update_end_reason_on_inactive(self):
url = reverse('bot:nomination-detail', args=(self.inactive_nomination.id,), host='api')
@@ -442,3 +483,76 @@ class NominationTests(APISubdomainTestCase):
infractions = response.json()
self.assertEqual(len(infractions), 2)
+
+ def test_return_nomination_entries_get_single_nomination(self):
+ url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+
+ self.assertEqual(len(data['entries']), 1)
+ self.assertEqual(data['entries'][0], {
+ "actor": self.user.id,
+ "reason": "He's pretty funky",
+ "inserted_at": self.active_nomination_entry.inserted_at.isoformat().replace(
+ "+00:00", "Z"
+ )
+ })
+
+ def test_return_nomination_entries_get_all_nominations(self):
+ url = reverse('api:nomination-list', host='api')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ data = response.json()
+
+ self.assertEqual(len(data), 2)
+ self.assertEqual(len(data[0]["entries"]), 1)
+ self.assertEqual(len(data[1]["entries"]), 1)
+
+ def test_patch_nomination_set_reviewed_of_active_nomination(self):
+ url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api')
+ data = {'reviewed': True}
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 200)
+
+ def test_patch_nomination_set_reviewed_of_inactive_nomination(self):
+ url = reverse('api:nomination-detail', args=(self.inactive_nomination.id,), host='api')
+ data = {'reviewed': True}
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'reviewed': ['This field cannot be set if nomination is inactive.']
+ })
+
+ def test_patch_nomination_set_reviewed_and_end(self):
+ url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api')
+ data = {'reviewed': True, 'active': False, 'end_reason': "What?"}
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'reviewed': ['This field cannot be set same time than ending nomination.']
+ })
+
+ def test_modifying_reason_without_actor(self):
+ url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api')
+ data = {'reason': 'That is my reason!'}
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'actor': ['This field is required when editing reason.']
+ })
+
+ def test_modifying_reason_with_unknown_actor(self):
+ url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api')
+ data = {'reason': 'That is my reason!', 'actor': 90909090909090}
+
+ response = self.client.patch(url, data=data)
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.json(), {
+ 'actor': ["Actor don't exist or have not nominated user."]
+ })
--
cgit v1.2.3
From f731e4dd31f9eba8e8c9916329a5c08578055077 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 24 Feb 2021 08:27:36 +0200
Subject: Set related_name option of nomination field of NominationEntry
In order to use entries in serializer without manually
setting entries key we have to use related_name option to
automatically fetch all related entries.
---
pydis_site/apps/api/models/bot/nomination.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index ed6f7d81..a813855d 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -53,7 +53,8 @@ class NominationEntry(ModelReprMixin, models.Model):
nomination = models.ForeignKey(
Nomination,
on_delete=models.CASCADE,
- help_text="Nomination to what this entry belongs."
+ help_text="Nomination to what this entry belongs.",
+ related_name="entries"
)
actor = models.ForeignKey(
User,
--
cgit v1.2.3
From 1fc5470a0af18aab427a48faf132bea2712330aa Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 24 Feb 2021 08:28:54 +0200
Subject: Set default ordering of NominationEntry to inserted_at decreasing
Set it here so we don't have to set it every place where we
fetch entries.
---
pydis_site/apps/api/models/bot/nomination.py | 4 ++++
1 file changed, 4 insertions(+)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index a813855d..e72a18ca 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -76,3 +76,7 @@ class NominationEntry(ModelReprMixin, models.Model):
"""Meta options for NominationEntry model."""
verbose_name_plural = "nomination entries"
+
+ # Set default ordering here to latest first
+ # so we don't need to define it everywhere
+ ordering = ("-inserted_at",)
--
cgit v1.2.3
From bb88dc66eb4692eaf5a8ef276944ec13aebb97cb Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 24 Feb 2021 08:29:53 +0200
Subject: Merge nomination migrations
This doesn't make sense to have 3 small migrations for one PR,
so I merged 2 existing migrations and 1 new, ordering and related_name adding migrations to one.
---
.../apps/api/migrations/0068_split_nomination_tables.py | 4 +++-
.../migrations/0069_change_nomination_entry_plural.py | 17 -----------------
2 files changed, 3 insertions(+), 18 deletions(-)
delete mode 100644 pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
index 1c392365..107e3a56 100644
--- a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
+++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
@@ -52,9 +52,11 @@ class Migration(migrations.Migration):
on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set',
to='api.User')),
('nomination', models.ForeignKey(help_text='Nomination to what this entry belongs.',
- on_delete=django.db.models.deletion.CASCADE, to='api.Nomination')),
+ on_delete=django.db.models.deletion.CASCADE, to='api.Nomination',
+ related_name='entries')),
],
bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
+ options={'ordering': ('-inserted_at',), 'verbose_name_plural': 'nomination entries'}
),
migrations.RunPython(migrate_nominations, unmigrate_nominations),
migrations.RemoveField(
diff --git a/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py b/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py
deleted file mode 100644
index 6bf4ac8c..00000000
--- a/pydis_site/apps/api/migrations/0069_change_nomination_entry_plural.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Generated by Django 3.0.11 on 2021-02-21 16:44
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('api', '0068_split_nomination_tables'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='nominationentry',
- options={'verbose_name_plural': 'nomination entries'},
- ),
- ]
--
cgit v1.2.3
From 7682b56fa881f809797e299bd829dfda6480b8d9 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 24 Feb 2021 08:31:06 +0200
Subject: Add entries field to Nomination serializer
After setting related_name in NominationEntry model nomination field,
we can just provide serializer and DRF automatically fetch
all related entries.
---
pydis_site/apps/api/serializers.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 49c247b5..17dd4b3d 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -357,12 +357,14 @@ class NominationEntrySerializer(ModelSerializer):
class NominationSerializer(ModelSerializer):
"""A class providing (de-)serialization of `Nomination` instances."""
+ entries = NominationEntrySerializer(many=True, read_only=True)
+
class Meta:
"""Metadata defined for the Django REST Framework."""
model = Nomination
fields = (
- 'id', 'active', 'user', 'inserted_at', 'end_reason', 'ended_at', 'reviewed'
+ 'id', 'active', 'user', 'inserted_at', 'end_reason', 'ended_at', 'reviewed', 'entries'
)
--
cgit v1.2.3
From 3edb416ba86c62d62c0258d4c69f967e47765186 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 24 Feb 2021 08:38:13 +0200
Subject: Simplify nominations viewset
After moving entries to nomination serializer we can get rid
from GET request handlers and let DRF handle this.
Also PATCH and POST handlers got some simplification by
removing manual entries setting.
---
pydis_site/apps/api/viewsets/bot/nomination.py | 51 ++------------------------
1 file changed, 3 insertions(+), 48 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index 7820ca0d..e3e71ca2 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -176,40 +176,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
frozen_fields = ('id', 'inserted_at', 'user', 'ended_at')
frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at', 'reviewed')
- def list(self, request: HttpRequest, *args, **kwargs) -> Response:
- """
- DRF method for listing Nominations.
-
- Called by the Django Rest Framework in response to the corresponding HTTP request.
- """
- queryset = self.filter_queryset(self.get_queryset())
- data = NominationSerializer(queryset, many=True).data
-
- for i, nomination in enumerate(data):
- entries = NominationEntrySerializer(
- NominationEntry.objects.filter(nomination_id=nomination["id"]),
- many=True
- ).data
- data[i]["entries"] = entries
-
- return Response(data)
-
- def retrieve(self, request: HttpRequest, *args, **kwargs) -> Response:
- """
- DRF method for retrieving a Nomination.
-
- Called by the Django Rest Framework in response to the corresponding HTTP request.
- """
- nomination = self.get_object()
-
- data = NominationSerializer(nomination).data
- data["entries"] = NominationEntrySerializer(
- NominationEntry.objects.filter(nomination_id=nomination.id),
- many=True
- ).data
-
- return Response(data)
-
def create(self, request: HttpRequest, *args, **kwargs) -> Response:
"""
DRF method for creating a Nomination.
@@ -238,11 +204,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
data=ChainMap(request.data, {"nomination": nomination.id})
)
entry_serializer.is_valid(raise_exception=True)
-
- entry = NominationEntry.objects.create(**entry_serializer.validated_data)
+ NominationEntry.objects.create(**entry_serializer.validated_data)
data = NominationSerializer(nomination).data
- data["entries"] = NominationEntrySerializer([entry], many=True).data
headers = self.get_success_headers(data)
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
@@ -264,10 +228,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
NominationEntry.objects.create(**entry_serializer.validated_data)
data = NominationSerializer(nomination_filter[0]).data
- data["entries"] = NominationEntrySerializer(
- NominationEntry.objects.filter(nomination_id=nomination_filter[0].id),
- many=True
- ).data
headers = self.get_success_headers(data)
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
@@ -349,11 +309,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
entry.reason = request.data['reason']
entry.save()
- nomination = serializer.save()
- return_data = NominationSerializer(nomination).data
- return_data["entries"] = NominationEntrySerializer(
- NominationEntry.objects.filter(nomination_id=nomination.id),
- many=True
- ).data
+ serializer.save()
- return Response(return_data)
+ return Response(serializer.data)
--
cgit v1.2.3
From 774e7c09d6950bb74e83af456d20b8f72fdef8ed Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 24 Feb 2021 08:41:40 +0200
Subject: Remove GET nomination test for entries
Entries isn't handled manually anymore so these tests have no point.
---
pydis_site/apps/api/tests/test_nominations.py | 26 --------------------------
1 file changed, 26 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
index 3892ec6e..1daa6f3f 100644
--- a/pydis_site/apps/api/tests/test_nominations.py
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -484,32 +484,6 @@ class NominationTests(APISubdomainTestCase):
self.assertEqual(len(infractions), 2)
- def test_return_nomination_entries_get_single_nomination(self):
- url = reverse('bot:nomination-detail', args=(self.active_nomination.id,), host='api')
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
- data = response.json()
-
- self.assertEqual(len(data['entries']), 1)
- self.assertEqual(data['entries'][0], {
- "actor": self.user.id,
- "reason": "He's pretty funky",
- "inserted_at": self.active_nomination_entry.inserted_at.isoformat().replace(
- "+00:00", "Z"
- )
- })
-
- def test_return_nomination_entries_get_all_nominations(self):
- url = reverse('api:nomination-list', host='api')
- response = self.client.get(url)
- self.assertEqual(response.status_code, 200)
-
- data = response.json()
-
- self.assertEqual(len(data), 2)
- self.assertEqual(len(data[0]["entries"]), 1)
- self.assertEqual(len(data[1]["entries"]), 1)
-
def test_patch_nomination_set_reviewed_of_active_nomination(self):
url = reverse('api:nomination-detail', args=(self.active_nomination.id,), host='api')
data = {'reviewed': True}
--
cgit v1.2.3
From cd03f7448ae4c96c23a80c4c46344bc35b8c603a Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 24 Feb 2021 08:44:22 +0200
Subject: Make default value of nomination entry reason to empty string
For string fields NULL as default is not suggested, so use empty string instead.
---
pydis_site/apps/api/migrations/0068_split_nomination_tables.py | 2 +-
pydis_site/apps/api/models/bot/nomination.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
index 107e3a56..27c29017 100644
--- a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
+++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
@@ -45,7 +45,7 @@ class Migration(migrations.Migration):
name='NominationEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('reason', models.TextField(blank=True, help_text='Why the actor nominated this user.', null=True)),
+ ('reason', models.TextField(blank=True, help_text='Why the actor nominated this user.', default="")),
('inserted_at',
models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination entry.')),
('actor', models.ForeignKey(help_text='The staff member that nominated this user.',
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index e72a18ca..443200ff 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -64,7 +64,7 @@ class NominationEntry(ModelReprMixin, models.Model):
)
reason = models.TextField(
help_text="Why the actor nominated this user.",
- null=True,
+ default="",
blank=True
)
inserted_at = models.DateTimeField(
--
cgit v1.2.3
From 98c60cf90f511f828d3e7bc3d7891b2809d27db6 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Wed, 24 Feb 2021 08:47:25 +0200
Subject: Add comment about manually defining nomination field of
NominationEntry serializer
---
pydis_site/apps/api/serializers.py | 3 +++
1 file changed, 3 insertions(+)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 17dd4b3d..f47bedca 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -342,6 +342,9 @@ class UserSerializer(ModelSerializer):
class NominationEntrySerializer(ModelSerializer):
"""A class providing (de-)serialization of `NominationEntry` instances."""
+ # We need to define it here, because we don't want that nomination ID
+ # return inside nomination response entry, because ID is already available
+ # as top-level field. Queryset is required if field is not read only.
nomination = PrimaryKeyRelatedField(
queryset=Nomination.objects.all(),
write_only=True
--
cgit v1.2.3
From 54eca6da46d83d8872746b89b95a22f7cf0c2b52 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Fri, 5 Mar 2021 07:48:32 +0200
Subject: Fix grammar of nomination endpoints documentation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Leon Sandรธy
Co-authored-by: Joe Banks
---
pydis_site/apps/api/viewsets/bot/nomination.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index e3e71ca2..c208df46 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -86,8 +86,8 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
Create a new, active nomination returns the created nominations.
The `user`, `reason` and `actor` fields are required and the `user`
and `actor` need to know by the site. Providing other valid fields
- is not allowed and invalid fields are ignored. If `user` already have
- active nomination, new nomination entry will be created assigned to
+ is not allowed and invalid fields are ignored. If `user` already has an
+ active nomination, a new nomination entry will be created and assigned as the
active nomination.
#### Request body
@@ -119,7 +119,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
below), the request bodies vary depending on the operation. For all operations it holds
that providing other valid fields is not allowed and invalid fields are ignored.
- ### 1. Updating the `reason` of `active` nomination. Actor field is required.
+ ### 1. Updating the `reason` of `active` nomination. The `actor` field is required.
#### Request body
>>> {
@@ -199,7 +199,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
serializer.is_valid(raise_exception=True)
nomination = Nomination.objects.create(**serializer.validated_data)
- # Serializer truncate unnecessary data away
+ # The serializer will truncate and get rid of excessive data
entry_serializer = NominationEntrySerializer(
data=ChainMap(request.data, {"nomination": nomination.id})
)
@@ -283,7 +283,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
# This is actually covered, but for some reason coverage don't think so.
elif 'reviewed' in request.data: # pragma: no cover
- # 4. We're setting nomination reviewed
+ # 4. We are altering the reviewed state of the nomination.
if not instance.active:
raise ValidationError(
{'reviewed': ['This field cannot be set if nomination is inactive.']}
--
cgit v1.2.3
From 72693a6b72332c1ca9411f73ae14d395c5dd8933 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Fri, 5 Mar 2021 07:49:46 +0200
Subject: Replace double quotes with single quotes
Co-authored-by: Joe Banks
---
pydis_site/apps/api/viewsets/bot/nomination.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index c208df46..ef7d1dab 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -302,7 +302,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
if not entry_filter.exists():
raise ValidationError(
- {'actor': ["Actor don't exist or have not nominated user."]}
+ {'actor': ['Actor don't exist or have not nominated user.']}
)
entry = entry_filter[0]
--
cgit v1.2.3
From 9075231a5fd68986b0845327d6a2bffe70447fc8 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Fri, 5 Mar 2021 08:18:17 +0200
Subject: Use double quotes instead apostrophe because string contain "don't"
---
pydis_site/apps/api/viewsets/bot/nomination.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index ef7d1dab..c208df46 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -302,7 +302,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
if not entry_filter.exists():
raise ValidationError(
- {'actor': ['Actor don't exist or have not nominated user.']}
+ {'actor': ["Actor don't exist or have not nominated user."]}
)
entry = entry_filter[0]
--
cgit v1.2.3
From d8751ad37ed5a493554575ea3adb264def342664 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Fri, 5 Mar 2021 08:23:28 +0200
Subject: Fix grammar of error messages and change tests to match with changes
---
pydis_site/apps/api/tests/test_nominations.py | 8 ++++----
pydis_site/apps/api/viewsets/bot/nomination.py | 8 ++++----
2 files changed, 8 insertions(+), 8 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
index 1daa6f3f..c07679c5 100644
--- a/pydis_site/apps/api/tests/test_nominations.py
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -78,7 +78,7 @@ class CreationTests(APISubdomainTestCase):
response2 = self.client.post(url, data=data)
self.assertEqual(response2.status_code, 400)
self.assertEqual(response2.json(), {
- 'actor': ['This actor have already created nomination entry for this nomination.']
+ 'actor': ['This actor has already endorsed this nomination.']
})
def test_returns_400_for_missing_user(self):
@@ -498,7 +498,7 @@ class NominationTests(APISubdomainTestCase):
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
- 'reviewed': ['This field cannot be set if nomination is inactive.']
+ 'reviewed': ['This field cannot be set if the nomination is inactive.']
})
def test_patch_nomination_set_reviewed_and_end(self):
@@ -508,7 +508,7 @@ class NominationTests(APISubdomainTestCase):
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
- 'reviewed': ['This field cannot be set same time than ending nomination.']
+ 'reviewed': ['This field cannot be set while you are ending a nomination.']
})
def test_modifying_reason_without_actor(self):
@@ -518,7 +518,7 @@ class NominationTests(APISubdomainTestCase):
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
- 'actor': ['This field is required when editing reason.']
+ 'actor': ['This field is required when editing the reason.']
})
def test_modifying_reason_with_unknown_actor(self):
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index c208df46..451264f4 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -222,7 +222,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
actor__id=entry_serializer.validated_data["actor"].id
).exists():
raise ValidationError(
- {'actor': ['This actor have already created nomination entry for this nomination.']}
+ {'actor': ['This actor has already endorsed this nomination.']}
)
NominationEntry.objects.create(**entry_serializer.validated_data)
@@ -270,7 +270,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
if 'reviewed' in request.data:
raise ValidationError(
- {'reviewed': ['This field cannot be set same time than ending nomination.']}
+ {'reviewed': ['This field cannot be set while you are ending a nomination.']}
)
instance.ended_at = timezone.now()
@@ -286,13 +286,13 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
# 4. We are altering the reviewed state of the nomination.
if not instance.active:
raise ValidationError(
- {'reviewed': ['This field cannot be set if nomination is inactive.']}
+ {'reviewed': ['This field cannot be set if the nomination is inactive.']}
)
if 'reason' in request.data:
if 'actor' not in request.data:
raise ValidationError(
- {'actor': ['This field is required when editing reason.']}
+ {'actor': ['This field is required when editing the reason.']}
)
entry_filter = NominationEntry.objects.filter(
--
cgit v1.2.3
From 53a9f5282adfdd3a2a4ebc819fcc9cd568a632ea Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 6 Mar 2021 14:09:38 +0200
Subject: Fix grammar of nomination models
---
pydis_site/apps/api/migrations/0068_split_nomination_tables.py | 4 ++--
pydis_site/apps/api/models/bot/nomination.py | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
index 27c29017..79825ed7 100644
--- a/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
+++ b/pydis_site/apps/api/migrations/0068_split_nomination_tables.py
@@ -51,7 +51,7 @@ class Migration(migrations.Migration):
('actor', models.ForeignKey(help_text='The staff member that nominated this user.',
on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set',
to='api.User')),
- ('nomination', models.ForeignKey(help_text='Nomination to what this entry belongs.',
+ ('nomination', models.ForeignKey(help_text='The nomination this entry belongs to.',
on_delete=django.db.models.deletion.CASCADE, to='api.Nomination',
related_name='entries')),
],
@@ -70,6 +70,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='nomination',
name='reviewed',
- field=models.BooleanField(default=False, help_text='Whether voting message have been made.'),
+ field=models.BooleanField(default=False, help_text='Whether a review was made.'),
),
]
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
index 443200ff..221d8534 100644
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -33,7 +33,7 @@ class Nomination(ModelReprMixin, models.Model):
)
reviewed = models.BooleanField(
default=False,
- help_text="Whether voting message have been made."
+ help_text="Whether a review was made."
)
def __str__(self):
@@ -48,12 +48,12 @@ class Nomination(ModelReprMixin, models.Model):
class NominationEntry(ModelReprMixin, models.Model):
- """A nomination entry created by single staff."""
+ """A nomination entry created by a single staff member."""
nomination = models.ForeignKey(
Nomination,
on_delete=models.CASCADE,
- help_text="Nomination to what this entry belongs.",
+ help_text="The nomination this entry belongs to.",
related_name="entries"
)
actor = models.ForeignKey(
--
cgit v1.2.3
From ce5a082458d8cc1433ce623d3b666fb6e0f82672 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 6 Mar 2021 14:11:56 +0200
Subject: Change as -> to in nomination viewset docs
---
pydis_site/apps/api/viewsets/bot/nomination.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index 451264f4..9b9aa280 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -87,7 +87,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
The `user`, `reason` and `actor` fields are required and the `user`
and `actor` need to know by the site. Providing other valid fields
is not allowed and invalid fields are ignored. If `user` already has an
- active nomination, a new nomination entry will be created and assigned as the
+ active nomination, a new nomination entry will be created and assigned to the
active nomination.
#### Request body
--
cgit v1.2.3
From 9202d92d3cb27111eaf5d3cc1aee859f9cbe8918 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 6 Mar 2021 14:12:56 +0200
Subject: Fix grammar of nomination viewset command about single entry for user
---
pydis_site/apps/api/viewsets/bot/nomination.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index 9b9aa280..0792a2a6 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -216,7 +216,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
)
entry_serializer.is_valid(raise_exception=True)
- # Don't allow user creating many nomination entries for one nomination
+ # Don't allow a user to create many nomination entries in a single nomination
if NominationEntry.objects.filter(
nomination_id=nomination_filter[0].id,
actor__id=entry_serializer.validated_data["actor"].id
--
cgit v1.2.3
From 48eff5d27715acd658f6284f4d4e1cdbbf71bee6 Mon Sep 17 00:00:00 2001
From: ks129 <45097959+ks129@users.noreply.github.com>
Date: Sat, 6 Mar 2021 14:13:50 +0200
Subject: Fix grammar of unknown actor error and change tests
---
pydis_site/apps/api/tests/test_nominations.py | 2 +-
pydis_site/apps/api/viewsets/bot/nomination.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
(limited to 'pydis_site/apps/api')
diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py
index c07679c5..9cefbd8f 100644
--- a/pydis_site/apps/api/tests/test_nominations.py
+++ b/pydis_site/apps/api/tests/test_nominations.py
@@ -528,5 +528,5 @@ class NominationTests(APISubdomainTestCase):
response = self.client.patch(url, data=data)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {
- 'actor': ["Actor don't exist or have not nominated user."]
+ 'actor': ["The actor doesn't exist or has not nominated the user."]
})
diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py
index 0792a2a6..144daab0 100644
--- a/pydis_site/apps/api/viewsets/bot/nomination.py
+++ b/pydis_site/apps/api/viewsets/bot/nomination.py
@@ -302,7 +302,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge
if not entry_filter.exists():
raise ValidationError(
- {'actor': ["Actor don't exist or have not nominated user."]}
+ {'actor': ["The actor doesn't exist or has not nominated the user."]}
)
entry = entry_filter[0]
--
cgit v1.2.3