aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api
diff options
context:
space:
mode:
Diffstat (limited to 'pydis_site/apps/api')
-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
16 files changed, 359 insertions, 20 deletions
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']: