aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps
diff options
context:
space:
mode:
authorGravatar wookie184 <[email protected]>2022-04-30 20:04:42 +0100
committerGravatar GitHub <[email protected]>2022-04-30 20:04:42 +0100
commit128fed6ef27fd8a8e42dfe18b0b86b884977af52 (patch)
tree62029929ca81d035364286848cd01b7ddbf41270 /pydis_site/apps
parentUsing hyperlink for gist link at the bottom (diff)
parentDon't do JSON as a database, kids. (#700) (diff)
Merge branch 'main' into discord-messages-colors-guide
Diffstat (limited to 'pydis_site/apps')
-rw-r--r--pydis_site/apps/api/migrations/0080_add_aoc_tables.py32
-rw-r--r--pydis_site/apps/api/migrations/0081_bumpedthread.py22
-rw-r--r--pydis_site/apps/api/migrations/0082_otn_allow_big_solidus.py19
-rw-r--r--pydis_site/apps/api/models/__init__.py5
-rw-r--r--pydis_site/apps/api/models/bot/__init__.py5
-rw-r--r--pydis_site/apps/api/models/bot/aoc_completionist_block.py26
-rw-r--r--pydis_site/apps/api/models/bot/aoc_link.py21
-rw-r--r--pydis_site/apps/api/models/bot/bumped_thread.py22
-rw-r--r--pydis_site/apps/api/models/bot/off_topic_channel_name.py2
-rw-r--r--pydis_site/apps/api/serializers.py49
-rw-r--r--pydis_site/apps/api/tests/test_bumped_threads.py63
-rw-r--r--pydis_site/apps/api/urls.py19
-rw-r--r--pydis_site/apps/api/viewsets/__init__.py5
-rw-r--r--pydis_site/apps/api/viewsets/bot/__init__.py3
-rw-r--r--pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py73
-rw-r--r--pydis_site/apps/api/viewsets/bot/aoc_link.py71
-rw-r--r--pydis_site/apps/api/viewsets/bot/bumped_thread.py66
-rw-r--r--pydis_site/apps/content/README.md32
-rw-r--r--pydis_site/apps/content/migrations/__init__.py0
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/discord-embed-limits.md21
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/discordpy_help_command.md66
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/vps-services.md31
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/why-not-json-as-database.md28
-rw-r--r--pydis_site/apps/content/resources/server-info/roles.md6
-rw-r--r--pydis_site/apps/staff/templatetags/deletedmessage_filters.py10
-rw-r--r--pydis_site/apps/staff/tests/test_logs_view.py12
26 files changed, 696 insertions, 13 deletions
diff --git a/pydis_site/apps/api/migrations/0080_add_aoc_tables.py b/pydis_site/apps/api/migrations/0080_add_aoc_tables.py
new file mode 100644
index 00000000..2c0c689a
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0080_add_aoc_tables.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.1.14 on 2022-03-06 16:07
+
+from django.db import migrations, models
+import django.db.models.deletion
+import pydis_site.apps.api.models.mixins
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0079_merge_20220125_2022'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='AocAccountLink',
+ fields=[
+ ('user', models.OneToOneField(help_text='The user that is blocked from getting the AoC Completionist Role', on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='api.user')),
+ ('aoc_username', models.CharField(help_text='The AoC username associated with the Discord User.', max_length=120)),
+ ],
+ bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
+ ),
+ migrations.CreateModel(
+ name='AocCompletionistBlock',
+ fields=[
+ ('user', models.OneToOneField(help_text='The user that is blocked from getting the AoC Completionist Role', on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='api.user')),
+ ('is_blocked', models.BooleanField(default=True, help_text='Whether this user is actively being blocked from getting the AoC Completionist Role', verbose_name='Blocked')),
+ ('reason', models.TextField(help_text='The reason for the AoC Completionist Role Block.', null=True)),
+ ],
+ bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0081_bumpedthread.py b/pydis_site/apps/api/migrations/0081_bumpedthread.py
new file mode 100644
index 00000000..03e66cc1
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0081_bumpedthread.py
@@ -0,0 +1,22 @@
+# Generated by Django 3.1.14 on 2022-02-19 16:26
+
+import django.core.validators
+from django.db import migrations, models
+import pydis_site.apps.api.models.mixins
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0080_add_aoc_tables'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BumpedThread',
+ fields=[
+ ('thread_id', models.BigIntegerField(help_text='The thread ID that should be bumped.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Thread IDs cannot be negative.')], verbose_name='Thread ID')),
+ ],
+ bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
+ ),
+ ]
diff --git a/pydis_site/apps/api/migrations/0082_otn_allow_big_solidus.py b/pydis_site/apps/api/migrations/0082_otn_allow_big_solidus.py
new file mode 100644
index 00000000..abbb98ec
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0082_otn_allow_big_solidus.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.1.14 on 2022-04-21 23:29
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0081_bumpedthread'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='offtopicchannelname',
+ name='name',
+ field=models.CharField(help_text='The actual channel name that will be used on our Discord server.', max_length=96, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex="^[a-z0-9\\U0001d5a0-\\U0001d5b9-ǃ?’'<>⧹⧸]+$")]),
+ ),
+ ]
diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py
index fd5bf220..a197e988 100644
--- a/pydis_site/apps/api/models/__init__.py
+++ b/pydis_site/apps/api/models/__init__.py
@@ -1,15 +1,18 @@
# flake8: noqa
from .bot import (
- FilterList,
BotSetting,
+ BumpedThread,
DocumentationLink,
DeletedMessage,
+ FilterList,
Infraction,
Message,
MessageDeletionContext,
Nomination,
NominationEntry,
OffensiveMessage,
+ AocAccountLink,
+ AocCompletionistBlock,
OffTopicChannelName,
Reminder,
Role,
diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py
index ac864de3..013bb85e 100644
--- a/pydis_site/apps/api/models/bot/__init__.py
+++ b/pydis_site/apps/api/models/bot/__init__.py
@@ -1,10 +1,13 @@
# flake8: noqa
-from .filter_list import FilterList
from .bot_setting import BotSetting
+from .bumped_thread import BumpedThread
from .deleted_message import DeletedMessage
from .documentation_link import DocumentationLink
+from .filter_list import FilterList
from .infraction import Infraction
from .message import Message
+from .aoc_completionist_block import AocCompletionistBlock
+from .aoc_link import AocAccountLink
from .message_deletion_context import MessageDeletionContext
from .nomination import Nomination, NominationEntry
from .off_topic_channel_name import OffTopicChannelName
diff --git a/pydis_site/apps/api/models/bot/aoc_completionist_block.py b/pydis_site/apps/api/models/bot/aoc_completionist_block.py
new file mode 100644
index 00000000..acbc0eba
--- /dev/null
+++ b/pydis_site/apps/api/models/bot/aoc_completionist_block.py
@@ -0,0 +1,26 @@
+from django.db import models
+
+from pydis_site.apps.api.models.bot.user import User
+from pydis_site.apps.api.models.mixins import ModelReprMixin
+
+
+class AocCompletionistBlock(ModelReprMixin, models.Model):
+ """A Discord user blocked from getting the AoC completionist Role."""
+
+ user = models.OneToOneField(
+ User,
+ on_delete=models.CASCADE,
+ help_text="The user that is blocked from getting the AoC Completionist Role",
+ primary_key=True
+ )
+
+ is_blocked = models.BooleanField(
+ default=True,
+ help_text="Whether this user is actively being blocked "
+ "from getting the AoC Completionist Role",
+ verbose_name="Blocked"
+ )
+ reason = models.TextField(
+ null=True,
+ help_text="The reason for the AoC Completionist Role Block."
+ )
diff --git a/pydis_site/apps/api/models/bot/aoc_link.py b/pydis_site/apps/api/models/bot/aoc_link.py
new file mode 100644
index 00000000..4e9d4882
--- /dev/null
+++ b/pydis_site/apps/api/models/bot/aoc_link.py
@@ -0,0 +1,21 @@
+from django.db import models
+
+from pydis_site.apps.api.models.bot.user import User
+from pydis_site.apps.api.models.mixins import ModelReprMixin
+
+
+class AocAccountLink(ModelReprMixin, models.Model):
+ """An AoC account link for a Discord User."""
+
+ user = models.OneToOneField(
+ User,
+ on_delete=models.CASCADE,
+ help_text="The user that is blocked from getting the AoC Completionist Role",
+ primary_key=True
+ )
+
+ aoc_username = models.CharField(
+ max_length=120,
+ help_text="The AoC username associated with the Discord User.",
+ blank=False
+ )
diff --git a/pydis_site/apps/api/models/bot/bumped_thread.py b/pydis_site/apps/api/models/bot/bumped_thread.py
new file mode 100644
index 00000000..cdf9a950
--- /dev/null
+++ b/pydis_site/apps/api/models/bot/bumped_thread.py
@@ -0,0 +1,22 @@
+from django.core.validators import MinValueValidator
+from django.db import models
+
+from pydis_site.apps.api.models.mixins import ModelReprMixin
+
+
+class BumpedThread(ModelReprMixin, models.Model):
+ """A list of thread IDs to be bumped."""
+
+ thread_id = models.BigIntegerField(
+ primary_key=True,
+ help_text=(
+ "The thread ID that should be bumped."
+ ),
+ validators=(
+ MinValueValidator(
+ limit_value=0,
+ message="Thread IDs cannot be negative."
+ ),
+ ),
+ verbose_name="Thread ID",
+ )
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 e9fec114..b380efad 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
@@ -11,7 +11,7 @@ class OffTopicChannelName(ModelReprMixin, models.Model):
primary_key=True,
max_length=96,
validators=(
- RegexValidator(regex=r"^[a-z0-9\U0001d5a0-\U0001d5b9-ǃ?’'<>]+$"),
+ RegexValidator(regex=r"^[a-z0-9\U0001d5a0-\U0001d5b9-ǃ?’'<>⧹⧸]+$"),
),
help_text="The actual channel name that will be used on our Discord server."
)
diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py
index 745aff42..e53ccffa 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -13,7 +13,10 @@ from rest_framework.settings import api_settings
from rest_framework.validators import UniqueTogetherValidator
from .models import (
+ AocAccountLink,
+ AocCompletionistBlock,
BotSetting,
+ BumpedThread,
DeletedMessage,
DocumentationLink,
FilterList,
@@ -39,6 +42,32 @@ class BotSettingSerializer(ModelSerializer):
fields = ('name', 'data')
+class ListBumpedThreadSerializer(ListSerializer):
+ """Custom ListSerializer to override to_representation() when list views are triggered."""
+
+ def to_representation(self, objects: list[BumpedThread]) -> int:
+ """
+ Used by the `ListModelMixin` to return just the list of bumped thread ids.
+
+ Only the thread_id field is useful, hence it is unnecessary to create a nested dictionary.
+
+ Additionally, this allows bumped thread routes to simply return an
+ array of thread_id ints instead of objects, saving on bandwidth.
+ """
+ return [obj.thread_id for obj in objects]
+
+
+class BumpedThreadSerializer(ModelSerializer):
+ """A class providing (de-)serialization of `BumpedThread` instances."""
+
+ class Meta:
+ """Metadata defined for the Django REST Framework."""
+
+ list_serializer_class = ListBumpedThreadSerializer
+ model = BumpedThread
+ fields = ('thread_id',)
+
+
class DeletedMessageSerializer(ModelSerializer):
"""
A class providing (de-)serialization of `DeletedMessage` instances.
@@ -250,6 +279,26 @@ class ReminderSerializer(ModelSerializer):
)
+class AocCompletionistBlockSerializer(ModelSerializer):
+ """A class providing (de-)serialization of `AocCompletionistBlock` instances."""
+
+ class Meta:
+ """Metadata defined for the Django REST Framework."""
+
+ model = AocCompletionistBlock
+ fields = ("user", "is_blocked", "reason")
+
+
+class AocAccountLinkSerializer(ModelSerializer):
+ """A class providing (de-)serialization of `AocAccountLink` instances."""
+
+ class Meta:
+ """Metadata defined for the Django REST Framework."""
+
+ model = AocAccountLink
+ fields = ("user", "aoc_username")
+
+
class RoleSerializer(ModelSerializer):
"""A class providing (de-)serialization of `Role` instances."""
diff --git a/pydis_site/apps/api/tests/test_bumped_threads.py b/pydis_site/apps/api/tests/test_bumped_threads.py
new file mode 100644
index 00000000..316e3f0b
--- /dev/null
+++ b/pydis_site/apps/api/tests/test_bumped_threads.py
@@ -0,0 +1,63 @@
+from django.urls import reverse
+
+from .base import AuthenticatedAPITestCase
+from ..models import BumpedThread
+
+
+class UnauthedBumpedThreadAPITests(AuthenticatedAPITestCase):
+ def setUp(self):
+ super().setUp()
+ self.client.force_authenticate(user=None)
+
+ def test_detail_lookup_returns_401(self):
+ url = reverse('api:bot:bumpedthread-detail', args=(1,))
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_list_returns_401(self):
+ url = reverse('api:bot:bumpedthread-list')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_create_returns_401(self):
+ url = reverse('api:bot:bumpedthread-list')
+ response = self.client.post(url, {"thread_id": 3})
+
+ self.assertEqual(response.status_code, 401)
+
+ def test_delete_returns_401(self):
+ url = reverse('api:bot:bumpedthread-detail', args=(1,))
+ response = self.client.delete(url)
+
+ self.assertEqual(response.status_code, 401)
+
+
+class BumpedThreadAPITests(AuthenticatedAPITestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.thread1 = BumpedThread.objects.create(
+ thread_id=1234,
+ )
+
+ def test_returns_bumped_threads_as_flat_list(self):
+ url = reverse('api:bot:bumpedthread-list')
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), [1234])
+
+ def test_returns_204_for_existing_data(self):
+ url = reverse('api:bot:bumpedthread-detail', args=(1234,))
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 204)
+ self.assertEqual(response.content, b"")
+
+ def test_returns_404_for_non_existing_data(self):
+ url = reverse('api:bot:bumpedthread-detail', args=(42,))
+
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.json(), {"detail": "Not found."})
diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py
index b0ab545b..1e564b29 100644
--- a/pydis_site/apps/api/urls.py
+++ b/pydis_site/apps/api/urls.py
@@ -3,7 +3,10 @@ from rest_framework.routers import DefaultRouter
from .views import HealthcheckView, RulesView
from .viewsets import (
+ AocAccountLinkViewSet,
+ AocCompletionistBlockViewSet,
BotSettingViewSet,
+ BumpedThreadViewSet,
DeletedMessageViewSet,
DocumentationLinkViewSet,
FilterListViewSet,
@@ -19,14 +22,22 @@ from .viewsets import (
# https://www.django-rest-framework.org/api-guide/routers/#defaultrouter
bot_router = DefaultRouter(trailing_slash=False)
bot_router.register(
- 'filter-lists',
- FilterListViewSet
+ "aoc-account-links",
+ AocAccountLinkViewSet
+)
+bot_router.register(
+ "aoc-completionist-blocks",
+ AocCompletionistBlockViewSet
)
bot_router.register(
'bot-settings',
BotSettingViewSet
)
bot_router.register(
+ 'bumped-threads',
+ BumpedThreadViewSet
+)
+bot_router.register(
'deleted-messages',
DeletedMessageViewSet
)
@@ -35,6 +46,10 @@ bot_router.register(
DocumentationLinkViewSet
)
bot_router.register(
+ 'filter-lists',
+ FilterListViewSet
+)
+bot_router.register(
'infractions',
InfractionViewSet
)
diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py
index f133e77f..ec52416a 100644
--- a/pydis_site/apps/api/viewsets/__init__.py
+++ b/pydis_site/apps/api/viewsets/__init__.py
@@ -1,12 +1,15 @@
# flake8: noqa
from .bot import (
- FilterListViewSet,
BotSettingViewSet,
+ BumpedThreadViewSet,
DeletedMessageViewSet,
DocumentationLinkViewSet,
+ FilterListViewSet,
InfractionViewSet,
NominationViewSet,
OffensiveMessageViewSet,
+ AocAccountLinkViewSet,
+ AocCompletionistBlockViewSet,
OffTopicChannelNameViewSet,
ReminderViewSet,
RoleViewSet,
diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py
index 84b87eab..262aa59f 100644
--- a/pydis_site/apps/api/viewsets/bot/__init__.py
+++ b/pydis_site/apps/api/viewsets/bot/__init__.py
@@ -1,12 +1,15 @@
# flake8: noqa
from .filter_list import FilterListViewSet
from .bot_setting import BotSettingViewSet
+from .bumped_thread import BumpedThreadViewSet
from .deleted_message import DeletedMessageViewSet
from .documentation_link import DocumentationLinkViewSet
from .infraction import InfractionViewSet
from .nomination import NominationViewSet
from .off_topic_channel_name import OffTopicChannelNameViewSet
from .offensive_message import OffensiveMessageViewSet
+from .aoc_link import AocAccountLinkViewSet
+from .aoc_completionist_block import AocCompletionistBlockViewSet
from .reminder import ReminderViewSet
from .role import RoleViewSet
from .user import UserViewSet
diff --git a/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py
new file mode 100644
index 00000000..3a4cec60
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py
@@ -0,0 +1,73 @@
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.mixins import (
+ CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin
+)
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot import AocCompletionistBlock
+from pydis_site.apps.api.serializers import AocCompletionistBlockSerializer
+
+
+class AocCompletionistBlockViewSet(
+ GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin
+):
+ """
+ View providing management for Users blocked from gettign the AoC completionist Role.
+
+ ## Routes
+
+ ### GET /bot/aoc-completionist-blocks/
+ Returns all the AoC completionist blocks
+
+ #### Response format
+ >>> [
+ ... {
+ ... "user": 2,
+ ... "is_blocked": False,
+ ... "reason": "Too good to be true"
+ ... }
+ ... ]
+
+
+ ### GET /bot/aoc-completionist-blocks/<user__id:int>
+ Retrieve a single Block by User ID
+
+ #### Response format
+ >>>
+ ... {
+ ... "user": 2,
+ ... "is_blocked": False,
+ ... "reason": "Too good to be true"
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 404: returned if an AoC completionist block with the given `user__id` was not found.
+
+ ### POST /bot/aoc-completionist-blocks
+ Adds a single AoC completionist block
+
+ #### Request body
+ >>> {
+ ... "user": int,
+ ... "is_blocked": bool,
+ ... "reason": string
+ ... }
+
+ #### Status codes
+ - 204: returned on success
+ - 400: if one of the given fields is invalid
+
+ ### DELETE /bot/aoc-completionist-blocks/<user__id:int>
+ Deletes the AoC Completionist block item with the given `user__id`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: returned if the AoC Completionist block with the given `user__id` was not found
+
+ """
+
+ serializer_class = AocCompletionistBlockSerializer
+ queryset = AocCompletionistBlock.objects.all()
+ filter_backends = (DjangoFilterBackend,)
+ filter_fields = ("user__id", "is_blocked")
diff --git a/pydis_site/apps/api/viewsets/bot/aoc_link.py b/pydis_site/apps/api/viewsets/bot/aoc_link.py
new file mode 100644
index 00000000..c7a96629
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/aoc_link.py
@@ -0,0 +1,71 @@
+from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.mixins import (
+ CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin
+)
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot import AocAccountLink
+from pydis_site.apps.api.serializers import AocAccountLinkSerializer
+
+
+class AocAccountLinkViewSet(
+ GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin
+):
+ """
+ View providing management for Users who linked their AoC accounts to their Discord Account.
+
+ ## Routes
+
+ ### GET /bot/aoc-account-links
+ Returns all the AoC account links
+
+ #### Response format
+ >>> [
+ ... {
+ ... "user": 2,
+ ... "aoc_username": "AoCUser1"
+ ... },
+ ... ...
+ ... ]
+
+
+ ### GET /bot/aoc-account-links/<user__id:int>
+ Retrieve a AoC account link by User ID
+
+ #### Response format
+ >>>
+ ... {
+ ... "user": 2,
+ ... "aoc_username": "AoCUser1"
+ ... }
+
+ #### Status codes
+ - 200: returned on success
+ - 404: returned if an AoC account link with the given `user__id` was not found.
+
+ ### POST /bot/aoc-account-links
+ Adds a single AoC account link block
+
+ #### Request body
+ >>> {
+ ... 'user': int,
+ ... 'aoc_username': str
+ ... }
+
+ #### Status codes
+ - 204: returned on success
+ - 400: if one of the given fields was invalid
+
+ ### DELETE /bot/aoc-account-links/<user__id:int>
+ Deletes the AoC account link item with the given `user__id`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: returned if the AoC account link with the given `user__id` was not found
+
+ """
+
+ serializer_class = AocAccountLinkSerializer
+ queryset = AocAccountLink.objects.all()
+ filter_backends = (DjangoFilterBackend,)
+ filter_fields = ("user__id", "aoc_username")
diff --git a/pydis_site/apps/api/viewsets/bot/bumped_thread.py b/pydis_site/apps/api/viewsets/bot/bumped_thread.py
new file mode 100644
index 00000000..9d77bb6b
--- /dev/null
+++ b/pydis_site/apps/api/viewsets/bot/bumped_thread.py
@@ -0,0 +1,66 @@
+from rest_framework.mixins import (
+ CreateModelMixin, DestroyModelMixin, ListModelMixin
+)
+from rest_framework.request import Request
+from rest_framework.response import Response
+from rest_framework.viewsets import GenericViewSet
+
+from pydis_site.apps.api.models.bot import BumpedThread
+from pydis_site.apps.api.serializers import BumpedThreadSerializer
+
+
+class BumpedThreadViewSet(
+ GenericViewSet, CreateModelMixin, DestroyModelMixin, ListModelMixin
+):
+ """
+ View providing CRUD (Minus the U) operations on threads to be bumped.
+
+ ## Routes
+ ### GET /bot/bumped-threads
+ Returns all BumpedThread items in the database.
+
+ #### Response format
+ >>> list[int]
+
+ #### Status codes
+ - 200: returned on success
+ - 401: returned if unauthenticated
+
+ ### GET /bot/bumped-threads/<thread_id:int>
+ Returns whether a specific BumpedThread exists in the database.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: returned if a BumpedThread with the given thread_id was not found.
+
+ ### POST /bot/bumped-threads
+ Adds a single BumpedThread item to the database.
+
+ #### Request body
+ >>> {
+ ... 'thread_id': int,
+ ... }
+
+ #### Status codes
+ - 201: returned on success
+ - 400: if one of the given fields is invalid
+
+ ### DELETE /bot/bumped-threads/<thread_id:int>
+ Deletes the BumpedThread item with the given `thread_id`.
+
+ #### Status codes
+ - 204: returned on success
+ - 404: if a BumpedThread with the given `thread_id` does not exist
+ """
+
+ serializer_class = BumpedThreadSerializer
+ queryset = BumpedThread.objects.all()
+
+ def retrieve(self, request: Request, *args, **kwargs) -> Response:
+ """
+ DRF method for checking if the given BumpedThread exists.
+
+ Called by the Django Rest Framework in response to the corresponding HTTP request.
+ """
+ self.get_object()
+ return Response(status=204)
diff --git a/pydis_site/apps/content/README.md b/pydis_site/apps/content/README.md
new file mode 100644
index 00000000..e7061207
--- /dev/null
+++ b/pydis_site/apps/content/README.md
@@ -0,0 +1,32 @@
+# The "content" app
+
+This application serves static, Markdown-based content. Django-wise there is
+relatively little code in there; most of it is concerned with serving our
+content.
+
+
+## Contributing pages
+
+The Markdown files hosting our content can be found in the
+[`resources/`](./resources) directory. The process of contributing to pages is
+covered extensively in our online guide which you can find
+[here](https://www.pythondiscord.com/pages/guides/pydis-guides/how-to-contribute-a-page/).
+Alternatively, read it directly at
+[`resources/guides/pydis-guides/how-to-contribute-a-page.md`](./resources/guides/pydis-guides/how-to-contribute-a-page.md).
+
+
+## Directory structure
+
+Let's look at the structure in here:
+
+- `resources` contains the static Markdown files that make up our site's
+ [pages](https://www.pythondiscord.com/pages/)
+
+- `tests` contains unit tests for verifying that the app works properly.
+
+- `views` contains Django views which generate and serve the pages from the
+ input Markdown.
+
+As for the modules, apart from the standard Django modules in here, the
+`utils.py` module contains utility functions for discovering Markdown files to
+serve.
diff --git a/pydis_site/apps/content/migrations/__init__.py b/pydis_site/apps/content/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/pydis_site/apps/content/migrations/__init__.py
+++ /dev/null
diff --git a/pydis_site/apps/content/resources/guides/python-guides/discord-embed-limits.md b/pydis_site/apps/content/resources/guides/python-guides/discord-embed-limits.md
new file mode 100644
index 00000000..ca97462b
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/discord-embed-limits.md
@@ -0,0 +1,21 @@
+---
+title: Discord Embed Limits
+description: A guide that shows the limits of embeds in Discord and how to avoid them.
+---
+
+If you plan on using embed responses for your bot you should know the limits of the embeds on Discord or you will get `Invalid Form Body` errors:
+
+- Embed **title** is limited to **256 characters**
+- Embed **description** is limited to **4096 characters**
+- An embed can contain a maximum of **25 fields**
+- A **field name/title** is limited to **256 character** and the **value of the field** is limited to **1024 characters**
+- Embed **footer** is limited to **2048 characters**
+- Embed **author name** is limited to **256 characters**
+- The **total of characters** allowed in an embed is **6000**
+
+Now if you need to get over this limit (for example for a help command), you would need to use pagination.
+There are several ways to do that:
+
+- A library called **[disputils](https://pypi.org/project/disputils)**
+- An experimental library made by the discord.py developer called **[discord-ext-menus](https://github.com/Rapptz/discord-ext-menus)**
+- Make your own setup using **[wait_for()](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.Bot.wait_for)** and wait for a reaction to be added
diff --git a/pydis_site/apps/content/resources/guides/python-guides/discordpy_help_command.md b/pydis_site/apps/content/resources/guides/python-guides/discordpy_help_command.md
new file mode 100644
index 00000000..4b475146
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/discordpy_help_command.md
@@ -0,0 +1,66 @@
+---
+title: Custom Help Command
+description: "Overwrite discord.py's help command to implement custom functionality"
+---
+
+First, a basic walkthrough can be found [here](https://gist.github.com/InterStella0/b78488fb28cadf279dfd3164b9f0cf96) by Stella#2000 on subclassing the HelpCommand. It will provide some foundational knowledge that is required before attempting a more customizable help command.
+
+## Custom Subclass of Help Command
+If the types of classes of the HelpCommand do not fit your needs, you can subclass HelpCommand and use the class mehods to customize the output. Below is a simple demonstration using the following methods that can also be found on the documenation:
+
+- [filter_commands](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.filter_commands)
+
+- [send_group_help](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.send_bot_help)
+
+- [send_command_help](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.send_command_help)
+
+- [send_group_help](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.send_group_help)
+
+- [send_error_message](https://discordpy.readthedocs.io/en/stable/ext/commands/api.html#discord.ext.commands.HelpCommand.send_error_message)
+
+```python
+class MyHelp(commands.HelpCommand):
+
+ async def send_bot_help(self, mapping):
+ """
+ This is triggered when !help is invoked.
+
+ This example demonstrates how to list the commands that the member invoking the help command can run.
+ """
+ filtered = await self.filter_commands(self.context.bot.commands, sort=True) # returns a list of command objects
+ names = [command.name for command in filtered] # iterating through the commands objects getting names
+ available_commands = "\n".join(names) # joining the list of names by a new line
+ embed = disnake.Embed(description=available_commands)
+ await self.context.send(embed=embed)
+
+ async def send_command_help(self, command):
+ """This is triggered when !help <command> is invoked."""
+ await self.context.send("This is the help page for a command")
+
+ async def send_group_help(self, group):
+ """This is triggered when !help <group> is invoked."""
+ await self.context.send("This is the help page for a group command")
+
+ async def send_cog_help(self, cog):
+ """This is triggered when !help <cog> is invoked."""
+ await self.context.send("This is the help page for a cog")
+
+ async def send_error_message(self, error):
+ """If there is an error, send a embed containing the error."""
+ channel = self.get_destination() # this defaults to the command context channel
+ await channel.send(error)
+
+bot.help_command = MyHelp()
+```
+
+You can handle when a user does not pass a command name when invoking the help command and make a fancy and customized embed; here a page that describes the bot and shows a list of commands is generally used. However if a command is passed in, you can display detailed information of the command. Below are references from the documentation below that can be utilised:
+
+- [Get the command object](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Bot.get_command)
+
+- [Get the command name](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Command.name)
+
+- [Get the command aliases](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Command.aliases)
+
+- [Get the command brief](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Command.brief)
+
+- [Get the command usage](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html#discord.ext.commands.Command.usage)
diff --git a/pydis_site/apps/content/resources/guides/python-guides/vps-services.md b/pydis_site/apps/content/resources/guides/python-guides/vps-services.md
new file mode 100644
index 00000000..0acd3e55
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/vps-services.md
@@ -0,0 +1,31 @@
+---
+title: VPS Services
+description: On different VPS services
+---
+
+If you need to run your bot 24/7 (with no downtime), you should consider using a virtual private server (VPS). This is a list of VPS services that are sufficient for running Discord bots.
+
+* Europe
+ * [netcup](https://www.netcup.eu/)
+ * Germany & Austria data centres.
+ * Great affiliate program.
+ * [Yandex Cloud](https://cloud.yandex.ru/)
+ * Vladimir, Ryazan, and Moscow region data centres.
+ * [Scaleway](https://www.scaleway.com/)
+ * France data centre.
+ * [Time 4 VPS](https://www.time4vps.eu/)
+ * Lithuania data centre.
+* US
+ * [GalaxyGate](https://galaxygate.net/)
+ * New York data centre.
+ * Great affiliate program.
+* Global
+ * [Linode](https://www.linode.com/)
+ * [Digital Ocean](https://www.digitalocean.com/)
+ * [OVHcloud](https://www.ovhcloud.com/)
+ * [Vultr](https://www.vultr.com/)
+
+---
+# Free hosts
+There are no reliable free options for VPS hosting. If you would rather not pay for a hosting service, you can consider self-hosting.
+Any modern hardware should be sufficient for running a bot. An old computer with a few GB of ram could be suitable, or a Raspberry Pi.
diff --git a/pydis_site/apps/content/resources/guides/python-guides/why-not-json-as-database.md b/pydis_site/apps/content/resources/guides/python-guides/why-not-json-as-database.md
new file mode 100644
index 00000000..ae34c2b4
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/why-not-json-as-database.md
@@ -0,0 +1,28 @@
+---
+title: Why JSON is unsuitable as a database
+description: The many reasons why you shouldn't use JSON as a database, and instead opt for SQL.
+relevant_links:
+ Tips on Storing Data: https://tutorial.vcokltfre.dev/tips/storage/
+---
+
+JSON, quite simply, is not a database. It's not designed to be a data storage format,
+rather a wayof transmitting data over a network. It's also often used as a way of doing configuration files for programs.
+
+There is no redundancy built in to JSON. JSON is just a format, and Python has libraries for it
+like json and ujson that let you load and dump it, sometimes to files, but that's all it does, write data to a file.
+There is no sort of DBMS (Database Management System), which means no sort of sophistication in how the data is stored,
+or built in ways to keep it safe and backed up, there's no built in encryption either - bear in mind
+in larger applications encryption may be necessary for GDPR/relevant data protection regulations compliance.
+
+JSON, unlike relational databases, has no way to store relational data,
+which is a very commonly needed way of storing data.
+Relational data, as the name may suggest, is data that relates to other data.
+For example if you have a table of users and a table of servers, the server table will probably have an owner field,
+where you'd reference a user from the users table. (**This is only relevant for relational data**).
+
+JSON is primarily a KV (key-value) format, for example `{"a": "b"}` where `a` is the key and `b` is the value,
+but what if you want to search not by that key but by a sub-key? Well, instead of being able to quickly use `var[key]`,
+which in a Python dictionary has a constant return time (for more info look up hash tables),
+you now have to iterate through every object in the dictionary and compare to find what you're looking for.
+Most relational database systems, like MySQL, MariaDB, and PostgreSQL have ways of indexing secondary fields
+apart from the primary key so that you can easily search by multiple attributes.
diff --git a/pydis_site/apps/content/resources/server-info/roles.md b/pydis_site/apps/content/resources/server-info/roles.md
index 716f5b1e..edc02066 100644
--- a/pydis_site/apps/content/resources/server-info/roles.md
+++ b/pydis_site/apps/content/resources/server-info/roles.md
@@ -68,7 +68,7 @@ In addition to the informal descriptions below, we've also written down a more f
### <span class="fas fa-circle" style="color:#1abc9c"></span> Domain Leads
**Description:** Staff in charge of a certain domain such as moderation, events, and outreach. A lead will have a second role specifying their domain.
-### <span class="fas fa-circle" style="color:#8dc2ba"></span> Project Leads
+### <span class="fas fa-circle" style="color:#00aeb4"></span> Project Leads
**Description:** Staff in charge of a certain project that require special attention, such as a YouTube video series or our new forms page.
### <span class="fas fa-circle" style="color:#ff9f1b"></span> Moderators
@@ -80,8 +80,8 @@ In addition to the informal descriptions below, we've also written down a more f
### <span class="fas fa-circle" style="color:#a1d1ff"></span> DevOps
**Description:** A role for staff involved with the DevOps toolchain of our core projects.
-### <span class="fas fa-circle" style="color:#f8d188"></span> Project Teams
-**Description:** Staff can join teams which work on specific projects in the organisation, such as our code jams, media projects, and more.
+### <span class="fas fa-circle" style="color:#7de29c"></span> Events Team
+**Description:** The events team are staff members who help plan and execute Python Discord events. This can range from the Code Jam, to Pixels, to our survey, specific workshops we want to run, and more.
### <span class="fas fa-circle" style="color:#eecd36"></span> Helpers
**Description:** This is the core staff role in our organization: All staff members have the Helpers role.
diff --git a/pydis_site/apps/staff/templatetags/deletedmessage_filters.py b/pydis_site/apps/staff/templatetags/deletedmessage_filters.py
index 8e14ced6..5026068e 100644
--- a/pydis_site/apps/staff/templatetags/deletedmessage_filters.py
+++ b/pydis_site/apps/staff/templatetags/deletedmessage_filters.py
@@ -1,4 +1,5 @@
from datetime import datetime
+from typing import Union
from django import template
@@ -6,13 +7,16 @@ register = template.Library()
@register.filter
-def hex_colour(color: int) -> str:
+def hex_colour(colour: Union[str, int]) -> str:
"""
- Converts an integer representation of a colour to the RGB hex value.
+ Converts the given representation of a colour to its RGB hex string.
As we are using a Discord dark theme analogue, black colours are returned as white instead.
"""
- colour = f"#{color:0>6X}"
+ if isinstance(colour, str):
+ colour = colour if colour.startswith("#") else f"#{colour}"
+ else:
+ colour = f"#{colour:0>6X}"
return colour if colour != "#000000" else "#FFFFFF"
diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py
index 45e9ce8f..3e5726cd 100644
--- a/pydis_site/apps/staff/tests/test_logs_view.py
+++ b/pydis_site/apps/staff/tests/test_logs_view.py
@@ -95,12 +95,22 @@ class TestLogsView(TestCase):
"description": "This embed is way too cool to be seen in public channels.",
}
+ cls.embed_three = {
+ "description": "This embed is way too cool to be seen in public channels.",
+ "color": "#e74c3c",
+ }
+
+ cls.embed_four = {
+ "description": "This embed is way too cool to be seen in public channels.",
+ "color": "e74c3c",
+ }
+
cls.deleted_message_two = DeletedMessage.objects.create(
author=cls.author,
id=614444836291870750,
channel_id=1984,
content='Does that mean this thing will halt?',
- embeds=[cls.embed_one, cls.embed_two],
+ embeds=[cls.embed_one, cls.embed_two, cls.embed_three, cls.embed_four],
attachments=['https://http.cat/100', 'https://http.cat/402'],
deletion_context=cls.deletion_context,
)