aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site/apps')
-rw-r--r--pydis_site/apps/admin/__init__.py0
-rw-r--r--pydis_site/apps/admin/urls.py8
-rw-r--r--pydis_site/apps/api/README.md71
-rw-r--r--pydis_site/apps/api/migrations/0080_add_aoc_tables.py32
-rw-r--r--pydis_site/apps/api/models/__init__.py2
-rw-r--r--pydis_site/apps/api/models/bot/__init__.py2
-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/serializers.py22
-rw-r--r--pydis_site/apps/api/tests/test_infractions.py4
-rw-r--r--pydis_site/apps/api/tests/test_models.py7
-rw-r--r--pydis_site/apps/api/tests/test_reminders.py17
-rw-r--r--pydis_site/apps/api/urls.py10
-rw-r--r--pydis_site/apps/api/viewsets/__init__.py2
-rw-r--r--pydis_site/apps/api/viewsets/bot/__init__.py2
-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/infraction.py17
-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/server-info/roles.md6
-rw-r--r--pydis_site/apps/home/tests/test_repodata_helpers.py5
-rw-r--r--pydis_site/apps/home/views/home.py7
23 files changed, 401 insertions, 36 deletions
diff --git a/pydis_site/apps/admin/__init__.py b/pydis_site/apps/admin/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/pydis_site/apps/admin/__init__.py
+++ /dev/null
diff --git a/pydis_site/apps/admin/urls.py b/pydis_site/apps/admin/urls.py
deleted file mode 100644
index a4f3e517..00000000
--- a/pydis_site/apps/admin/urls.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from django.contrib import admin
-from django.urls import path
-
-
-app_name = 'admin'
-urlpatterns = (
- path('', admin.site.urls),
-)
diff --git a/pydis_site/apps/api/README.md b/pydis_site/apps/api/README.md
new file mode 100644
index 00000000..1c6358b3
--- /dev/null
+++ b/pydis_site/apps/api/README.md
@@ -0,0 +1,71 @@
+# The "api" app
+
+This application takes care of most of the heavy lifting in the site, that is,
+allowing our bot to manipulate and query information stored in the site's
+database.
+
+We make heavy use of [Django REST
+Framework](https://www.django-rest-framework.org) here, which builds on top of
+Django to allow us to easily build out the
+[REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API
+consumed by our bot. Working with the API app requires basic knowledge of DRF -
+the [quickstart
+guide](https://www.django-rest-framework.org/tutorial/quickstart/) is a great
+resource to get started.
+
+## Directory structure
+
+Let's look over each of the subdirectories here:
+
+- `migrations` is the standard Django migrations folder. You usually won't need
+ to edit this manually, as `python manage.py makemigrations` handles this for
+ you in case you change our models. (Note that when generating migrations and
+ Django doesn't generate a human-readable name for you, please supply one
+ manually using `-n add_this_field`.)
+
+- `models` contains our Django model definitions. We put models into subfolders
+ relevant as to where they are used - in our case, the `bot` folder contains
+ models used by our bot when working with the API. Each model is contained
+ within its own module, such as `api/models/bot/message_deletion_context.py`,
+ which contains the `MessageDeletionContext` model.
+
+- `tests` contains tests for our API. If you're unfamilar with Django testing,
+ the [Django tutorial introducing automated
+ testing](https://docs.djangoproject.com/en/dev/intro/tutorial05/) is a great
+ resource, and you can also check out code in there to see how we test it.
+
+- `viewsets` contains our [DRF
+ viewsets](https://www.django-rest-framework.org/api-guide/viewsets/), and is
+ structured similarly to the `models` folder: The `bot` subfolder contains
+ viewsets relevant to the Python Bot, and each viewset is contained within its
+ own module.
+
+The remaining modules mostly do what their name suggests:
+
+- `admin.py`, which hooks up our models to the [Django admin
+ site](https://docs.djangoproject.com/en/dev/ref/contrib/admin/).
+
+- `apps.py` contains the Django [application
+ config](https://docs.djangoproject.com/en/dev/ref/applications/) for the `api`
+ app, and is used to run any code that should run when the app is loaded.
+
+- `pagination.py` contains custom
+ [paginators](https://www.django-rest-framework.org/api-guide/pagination/) used
+ within our DRF viewsets.
+
+- `serializers.py` contains [DRF
+ serializers](https://www.django-rest-framework.org/api-guide/serializers/) for
+ our models, and also includes validation logic for the models.
+
+- `signals.py` contains [Django
+ Signals](https://docs.djangoproject.com/en/dev/topics/signals/) for running
+ custom functionality in response to events such as deletion of a model
+ instance.
+
+- `urls.py` configures Django's [URL
+ dispatcher](https://docs.djangoproject.com/en/dev/topics/http/urls/) for our
+ API endpoints.
+
+- `views.py` is for any standard Django views that don't make sense to be put
+ into DRF viewsets as they provide static data or other functionality that
+ doesn't interact with our models.
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/models/__init__.py b/pydis_site/apps/api/models/__init__.py
index fd5bf220..4f616986 100644
--- a/pydis_site/apps/api/models/__init__.py
+++ b/pydis_site/apps/api/models/__init__.py
@@ -10,6 +10,8 @@ from .bot import (
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..ec0e701c 100644
--- a/pydis_site/apps/api/models/bot/__init__.py
+++ b/pydis_site/apps/api/models/bot/__init__.py
@@ -5,6 +5,8 @@ from .deleted_message import DeletedMessage
from .documentation_link import DocumentationLink
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/serializers.py b/pydis_site/apps/api/serializers.py
index 745aff42..c97f7dba 100644
--- a/pydis_site/apps/api/serializers.py
+++ b/pydis_site/apps/api/serializers.py
@@ -13,6 +13,8 @@ from rest_framework.settings import api_settings
from rest_framework.validators import UniqueTogetherValidator
from .models import (
+ AocAccountLink,
+ AocCompletionistBlock,
BotSetting,
DeletedMessage,
DocumentationLink,
@@ -250,6 +252,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_infractions.py b/pydis_site/apps/api/tests/test_infractions.py
index aa0604f6..f1107734 100644
--- a/pydis_site/apps/api/tests/test_infractions.py
+++ b/pydis_site/apps/api/tests/test_infractions.py
@@ -80,7 +80,7 @@ class InfractionTests(AuthenticatedAPITestCase):
type='superstar',
reason='This one doesn\'t matter anymore.',
active=True,
- expires_at=datetime.datetime.utcnow() + datetime.timedelta(hours=5)
+ expires_at=dt.now(timezone.utc) + datetime.timedelta(hours=5)
)
cls.voiceban_expires_later = Infraction.objects.create(
user_id=cls.user.id,
@@ -88,7 +88,7 @@ class InfractionTests(AuthenticatedAPITestCase):
type='voice_ban',
reason='Jet engine mic',
active=True,
- expires_at=datetime.datetime.utcnow() + datetime.timedelta(days=5)
+ expires_at=dt.now(timezone.utc) + datetime.timedelta(days=5)
)
def test_list_all(self):
diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py
index 5c9ddea4..0fad467c 100644
--- a/pydis_site/apps/api/tests/test_models.py
+++ b/pydis_site/apps/api/tests/test_models.py
@@ -1,8 +1,7 @@
-from datetime import datetime as dt
+from datetime import datetime as dt, timezone
from django.core.exceptions import ValidationError
from django.test import SimpleTestCase, TestCase
-from django.utils import timezone
from pydis_site.apps.api.models import (
DeletedMessage,
@@ -41,7 +40,7 @@ class NitroMessageLengthTest(TestCase):
self.context = MessageDeletionContext.objects.create(
id=50,
actor=self.user,
- creation=dt.utcnow()
+ creation=dt.now(timezone.utc)
)
def test_create(self):
@@ -99,7 +98,7 @@ class StringDunderMethodTests(SimpleTestCase):
name='shawn',
discriminator=555,
),
- creation=dt.utcnow()
+ creation=dt.now(timezone.utc)
),
embeds=[]
),
diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py
index 709685bc..e17569f0 100644
--- a/pydis_site/apps/api/tests/test_reminders.py
+++ b/pydis_site/apps/api/tests/test_reminders.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timezone
from django.forms.models import model_to_dict
from django.urls import reverse
@@ -91,7 +91,7 @@ class ReminderDeletionTests(AuthenticatedAPITestCase):
cls.reminder = Reminder.objects.create(
author=cls.author,
content="Don't forget to set yourself a reminder",
- expiration=datetime.utcnow().isoformat(),
+ expiration=datetime.now(timezone.utc),
jump_url="https://www.decliningmentalfaculties.com",
channel_id=123
)
@@ -122,7 +122,7 @@ class ReminderListTests(AuthenticatedAPITestCase):
cls.reminder_one = Reminder.objects.create(
author=cls.author,
content="We should take Bikini Bottom, and push it somewhere else!",
- expiration=datetime.utcnow().isoformat(),
+ expiration=datetime.now(timezone.utc),
jump_url="https://www.icantseemyforehead.com",
channel_id=123
)
@@ -130,16 +130,17 @@ class ReminderListTests(AuthenticatedAPITestCase):
cls.reminder_two = Reminder.objects.create(
author=cls.author,
content="Gahhh-I love being purple!",
- expiration=datetime.utcnow().isoformat(),
+ expiration=datetime.now(timezone.utc),
jump_url="https://www.goofygoobersicecreampartyboat.com",
channel_id=123,
active=False
)
+ drf_format = '%Y-%m-%dT%H:%M:%S.%fZ'
cls.rem_dict_one = model_to_dict(cls.reminder_one)
- cls.rem_dict_one['expiration'] += 'Z' # Massaging a quirk of the response time format
+ cls.rem_dict_one['expiration'] = cls.rem_dict_one['expiration'].strftime(drf_format)
cls.rem_dict_two = model_to_dict(cls.reminder_two)
- cls.rem_dict_two['expiration'] += 'Z' # Massaging a quirk of the response time format
+ cls.rem_dict_two['expiration'] = cls.rem_dict_two['expiration'].strftime(drf_format)
def test_reminders_in_full_list(self):
url = reverse('api:bot:reminder-list')
@@ -175,7 +176,7 @@ class ReminderRetrieveTests(AuthenticatedAPITestCase):
cls.reminder = Reminder.objects.create(
author=cls.author,
content="Reminder content",
- expiration=datetime.utcnow().isoformat(),
+ expiration=datetime.now(timezone.utc),
jump_url="http://example.com/",
channel_id=123
)
@@ -203,7 +204,7 @@ class ReminderUpdateTests(AuthenticatedAPITestCase):
cls.reminder = Reminder.objects.create(
author=cls.author,
content="Squash those do-gooders",
- expiration=datetime.utcnow().isoformat(),
+ expiration=datetime.now(timezone.utc),
jump_url="https://www.decliningmentalfaculties.com",
channel_id=123
)
diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py
index b0ab545b..7c55fc92 100644
--- a/pydis_site/apps/api/urls.py
+++ b/pydis_site/apps/api/urls.py
@@ -3,6 +3,8 @@ from rest_framework.routers import DefaultRouter
from .views import HealthcheckView, RulesView
from .viewsets import (
+ AocAccountLinkViewSet,
+ AocCompletionistBlockViewSet,
BotSettingViewSet,
DeletedMessageViewSet,
DocumentationLinkViewSet,
@@ -35,6 +37,14 @@ bot_router.register(
DocumentationLinkViewSet
)
bot_router.register(
+ "aoc-account-links",
+ AocAccountLinkViewSet
+)
+bot_router.register(
+ "aoc-completionist-blocks",
+ AocCompletionistBlockViewSet
+)
+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..5fc1d64f 100644
--- a/pydis_site/apps/api/viewsets/__init__.py
+++ b/pydis_site/apps/api/viewsets/__init__.py
@@ -7,6 +7,8 @@ from .bot import (
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..f1d84729 100644
--- a/pydis_site/apps/api/viewsets/bot/__init__.py
+++ b/pydis_site/apps/api/viewsets/bot/__init__.py
@@ -7,6 +7,8 @@ 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..9f22c1a1
--- /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",)
diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py
index 7e7adbca..7f31292f 100644
--- a/pydis_site/apps/api/viewsets/bot/infraction.py
+++ b/pydis_site/apps/api/viewsets/bot/infraction.py
@@ -3,6 +3,7 @@ from datetime import datetime
from django.db import IntegrityError
from django.db.models import QuerySet
from django.http.request import HttpRequest
+from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
@@ -184,20 +185,24 @@ class InfractionViewSet(
filter_expires_after = self.request.query_params.get('expires_after')
if filter_expires_after:
try:
- additional_filters['expires_at__gte'] = datetime.fromisoformat(
- filter_expires_after
- )
+ expires_after_parsed = datetime.fromisoformat(filter_expires_after)
except ValueError:
raise ValidationError({'expires_after': ['failed to convert to datetime']})
+ additional_filters['expires_at__gte'] = timezone.make_aware(
+ expires_after_parsed,
+ timezone=timezone.utc,
+ )
filter_expires_before = self.request.query_params.get('expires_before')
if filter_expires_before:
try:
- additional_filters['expires_at__lte'] = datetime.fromisoformat(
- filter_expires_before
- )
+ expires_before_parsed = datetime.fromisoformat(filter_expires_before)
except ValueError:
raise ValidationError({'expires_before': ['failed to convert to datetime']})
+ additional_filters['expires_at__lte'] = timezone.make_aware(
+ expires_before_parsed,
+ timezone=timezone.utc,
+ )
if 'expires_at__lte' in additional_filters and 'expires_at__gte' in additional_filters:
if additional_filters['expires_at__gte'] > additional_filters['expires_at__lte']:
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/server-info/roles.md b/pydis_site/apps/content/resources/server-info/roles.md
index d9e0af15..409e037e 100644
--- a/pydis_site/apps/content/resources/server-info/roles.md
+++ b/pydis_site/apps/content/resources/server-info/roles.md
@@ -72,7 +72,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
@@ -84,8 +84,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/home/tests/test_repodata_helpers.py b/pydis_site/apps/home/tests/test_repodata_helpers.py
index 5634bc9b..d43bd28e 100644
--- a/pydis_site/apps/home/tests/test_repodata_helpers.py
+++ b/pydis_site/apps/home/tests/test_repodata_helpers.py
@@ -122,7 +122,10 @@ class TestRepositoryMetadataHelpers(TestCase):
"""Tests that fallback to the database is performed when we get garbage back."""
mock_get.return_value.json.return_value = ['garbage']
- metadata = self.home_view._get_repo_data()
+ # Capture logs and ensure the problematic response is logged
+ with self.assertLogs():
+ metadata = self.home_view._get_repo_data()
+
self.assertEquals(len(metadata), 0)
def test_cleans_up_stale_metadata(self):
diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py
index e28a3a00..69e706c5 100644
--- a/pydis_site/apps/home/views/home.py
+++ b/pydis_site/apps/home/views/home.py
@@ -10,7 +10,6 @@ from django.views import View
from pydis_site import settings
from pydis_site.apps.home.models import RepositoryMetadata
-from pydis_site.constants import GITHUB_TOKEN, TIMEOUT_PERIOD
log = logging.getLogger(__name__)
@@ -43,8 +42,8 @@ class HomeView(View):
# specifically, GitHub will reject any requests from us due to the
# invalid header. We can make a limited number of anonymous requests
# though, which is useful for testing.
- if GITHUB_TOKEN:
- self.headers = {"Authorization": f"token {GITHUB_TOKEN}"}
+ if settings.GITHUB_TOKEN:
+ self.headers = {"Authorization": f"token {settings.GITHUB_TOKEN}"}
else:
self.headers = {}
@@ -60,7 +59,7 @@ class HomeView(View):
api_data: List[dict] = requests.get(
self.github_api,
headers=self.headers,
- timeout=TIMEOUT_PERIOD
+ timeout=settings.TIMEOUT_PERIOD
).json()
except requests.exceptions.Timeout:
log.error("Request to fetch GitHub repository metadata for timed out!")