aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps
diff options
context:
space:
mode:
authorGravatar Boris Muratov <[email protected]>2022-05-17 03:39:58 +0300
committerGravatar GitHub <[email protected]>2022-05-17 03:39:58 +0300
commitaa620e4b8910fcce857f91c213fb275d73e31fd9 (patch)
tree320bfd94e7ad338dac507f91b33523fcc1cd70f8 /pydis_site/apps
parentAdd Making Changes step (diff)
parentMerge pull request #720 from python-discord/id-in-delete-logs (diff)
Merge branch 'main' into contrib-streamline
Diffstat (limited to 'pydis_site/apps')
-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__.py3
-rw-r--r--pydis_site/apps/api/models/bot/__init__.py3
-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.py27
-rw-r--r--pydis_site/apps/api/tests/test_bumped_threads.py63
-rw-r--r--pydis_site/apps/api/urls.py21
-rw-r--r--pydis_site/apps/api/viewsets/__init__.py3
-rw-r--r--pydis_site/apps/api/viewsets/bot/__init__.py1
-rw-r--r--pydis_site/apps/api/viewsets/bot/aoc_link.py2
-rw-r--r--pydis_site/apps/api/viewsets/bot/bumped_thread.py66
-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/discord-messages-with-colors.md68
-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/vps_services.md58
-rw-r--r--pydis_site/apps/content/resources/guides/python-guides/why-not-json-as-database.md28
-rw-r--r--pydis_site/apps/resources/resources/python_morsels.yaml1
-rw-r--r--pydis_site/apps/staff/templatetags/deletedmessage_filters.py10
-rw-r--r--pydis_site/apps/staff/tests/test_logs_view.py12
22 files changed, 532 insertions, 17 deletions
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 4f616986..a197e988 100644
--- a/pydis_site/apps/api/models/__init__.py
+++ b/pydis_site/apps/api/models/__init__.py
@@ -1,9 +1,10 @@
# flake8: noqa
from .bot import (
- FilterList,
BotSetting,
+ BumpedThread,
DocumentationLink,
DeletedMessage,
+ FilterList,
Infraction,
Message,
MessageDeletionContext,
diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py
index ec0e701c..013bb85e 100644
--- a/pydis_site/apps/api/models/bot/__init__.py
+++ b/pydis_site/apps/api/models/bot/__init__.py
@@ -1,8 +1,9 @@
# 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
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 c97f7dba..e53ccffa 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -16,6 +16,7 @@ from .models import (
AocAccountLink,
AocCompletionistBlock,
BotSetting,
+ BumpedThread,
DeletedMessage,
DocumentationLink,
FilterList,
@@ -41,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.
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 7c55fc92..1e564b29 100644
--- a/pydis_site/apps/api/urls.py
+++ b/pydis_site/apps/api/urls.py
@@ -6,6 +6,7 @@ from .viewsets import (
AocAccountLinkViewSet,
AocCompletionistBlockViewSet,
BotSettingViewSet,
+ BumpedThreadViewSet,
DeletedMessageViewSet,
DocumentationLinkViewSet,
FilterListViewSet,
@@ -21,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
)
@@ -37,12 +46,8 @@ bot_router.register(
DocumentationLinkViewSet
)
bot_router.register(
- "aoc-account-links",
- AocAccountLinkViewSet
-)
-bot_router.register(
- "aoc-completionist-blocks",
- AocCompletionistBlockViewSet
+ 'filter-lists',
+ FilterListViewSet
)
bot_router.register(
'infractions',
diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py
index 5fc1d64f..ec52416a 100644
--- a/pydis_site/apps/api/viewsets/__init__.py
+++ b/pydis_site/apps/api/viewsets/__init__.py
@@ -1,9 +1,10 @@
# flake8: noqa
from .bot import (
- FilterListViewSet,
BotSettingViewSet,
+ BumpedThreadViewSet,
DeletedMessageViewSet,
DocumentationLinkViewSet,
+ FilterListViewSet,
InfractionViewSet,
NominationViewSet,
OffensiveMessageViewSet,
diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py
index f1d84729..262aa59f 100644
--- a/pydis_site/apps/api/viewsets/bot/__init__.py
+++ b/pydis_site/apps/api/viewsets/bot/__init__.py
@@ -1,6 +1,7 @@
# 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
diff --git a/pydis_site/apps/api/viewsets/bot/aoc_link.py b/pydis_site/apps/api/viewsets/bot/aoc_link.py
index 9f22c1a1..c7a96629 100644
--- a/pydis_site/apps/api/viewsets/bot/aoc_link.py
+++ b/pydis_site/apps/api/viewsets/bot/aoc_link.py
@@ -68,4 +68,4 @@ class AocAccountLinkViewSet(
serializer_class = AocAccountLinkSerializer
queryset = AocAccountLink.objects.all()
filter_backends = (DjangoFilterBackend,)
- filter_fields = ("user__id",)
+ 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/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/discord-messages-with-colors.md b/pydis_site/apps/content/resources/guides/python-guides/discord-messages-with-colors.md
new file mode 100644
index 00000000..62ff61f9
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/discord-messages-with-colors.md
@@ -0,0 +1,68 @@
+---
+title: Discord Messages with Colors
+description: A guide on how to add colors to your codeblocks on Discord
+---
+
+Discord is now slowly rolling out the ability to send colored text within code blocks. This is done using ANSI color codes which is also how you print colored text in your terminal.
+
+To send colored text in a code block you need to first specify the `ansi` language and use the prefixes similar to the one below:
+```ansi
+\u001b[{format};{color}m
+```
+*`\u001b` is the unicode escape for ESCAPE/ESC, meant to be used in the source of your bot (see <http://www.unicode-symbol.com/u/001B.html>).* ***If you wish to send colored text without using your bot you need to copy the character from the website.***
+
+After you've written this, you can now type the text you wish to color. If you want to reset the color back to normal, then you need to use the `\u001b[0m` prefix again.
+
+Here is the list of values you can use to replace `{format}`:
+
+* 0: Normal
+* 1: **Bold**
+* 4: <ins>Underline</ins>
+
+Here is the list of values you can use to replace `{color}`:
+
+*The following values will change the **text** color.*
+
+* 30: Gray
+* 31: Red
+* 32: Green
+* 33: Yellow
+* 34: Blue
+* 35: Pink
+* 36: Cyan
+* 37: White
+
+*The following values will change the **text background** color.*
+
+* 40: Firefly dark blue
+* 41: Orange
+* 42: Marble blue
+* 43: Greyish turquoise
+* 44: Gray
+* 45: Indigo
+* 46: Light gray
+* 47: White
+
+Let's take an example, I want a bold green colored text with the very dark blue background.
+I simply use `\u001b[0;40m` (background color) and `\u001b[1;32m` (text color) as prefix. Note that the order is **important**, first you give the background color and then the text color.
+
+Alternatively you can also directly combine them into a single prefix like the following: `\u001b[1;40;32m` and you can also use multiple values. Something like `\u001b[1;40;4;32m` would underline the text, make it bold, make it green and have a dark blue background.
+
+Raw message:
+````nohighlight
+```ansi
+\u001b[0;40m\u001b[1;32mThat's some cool formatted text right?
+or
+\u001b[1;40;32mThat's some cool formatted text right?
+```
+````
+
+Result:
+
+![Background and text color result](/static/images/content/discord_colored_messages/result.png)
+
+The way the colors look like on Discord is shown in the image below:
+
+![ANSI Colors](/static/images/content/discord_colored_messages/ansi-colors.png)
+
+Note: If the change as not been brought to you yet, or other users, then you can use other code blocks in the meantime to get colored text. See **[this gist](https://gist.github.com/matthewzring/9f7bbfd102003963f9be7dbcf7d40e51)**.
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/vps_services.md b/pydis_site/apps/content/resources/guides/python-guides/vps_services.md
new file mode 100644
index 00000000..710fd914
--- /dev/null
+++ b/pydis_site/apps/content/resources/guides/python-guides/vps_services.md
@@ -0,0 +1,58 @@
+---
+title: VPS and Free Hosting Service for Discord bots
+description: This article lists recommended VPS services and covers the disasdvantages of utilising a free hosting service to run a discord bot.
+toc: 2
+---
+
+## Recommended VPS services
+
+If you need to run your bot 24/7 (with no downtime), you should consider using a virtual private server (VPS). Here 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/)
+
+
+## Why not to use free hosting services for bots?
+While these may seem like nice and free services, it has a lot more caveats than you may think. For example, the drawbacks of using common free hosting services to host a discord bot are discussed below.
+
+### Replit
+
+- The machines are super underpowered, resulting in your bot lagging a lot as it gets bigger.
+
+- You need to run a webserver alongside your bot to prevent it from being shut off. This uses extra machine power.
+
+- Repl.it uses an ephemeral file system. This means any file you saved through your bot will be overwritten when you next launch.
+
+- They use a shared IP for everything running on the service.
+This one is important - if someone is running a user bot on their service and gets banned, everyone on that IP will be banned. Including you.
+
+### Heroku
+- Bots are not what the platform is designed for. Heroku is designed to provide web servers (like Django, Flask, etc). This is why they give you a domain name and open a port on their local emulator.
+
+- Heroku's environment is heavily containerized, making it significantly underpowered for a standard use case.
+
+- Heroku's environment is volatile. In order to handle the insane amount of users trying to use it for their own applications, Heroku will dispose your environment every time your application dies unless you pay.
+
+- Heroku has minimal system dependency control. If any of your Python requirements need C bindings (such as PyNaCl
+ binding to libsodium, or lxml binding to libxml), they are unlikely to function properly, if at all, in a native
+ environment. As such, you often need to resort to adding third-party buildpacks to facilitate otherwise normal
+ CPython extension functionality. (This is the reason why voice doesn't work natively on heroku)
+
+- Heroku only offers a limited amount of time on their free programme for your applications. If you exceed this limit, which you probably will, they'll shut down your application until your free credit resets.
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/resources/resources/python_morsels.yaml b/pydis_site/apps/resources/resources/python_morsels.yaml
index bbc8133b..4cdff36b 100644
--- a/pydis_site/apps/resources/resources/python_morsels.yaml
+++ b/pydis_site/apps/resources/resources/python_morsels.yaml
@@ -17,3 +17,4 @@ tags:
- intermediate
type:
- interactive
+ - video
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,
)