aboutsummaryrefslogtreecommitdiffstats
path: root/pydis_site/apps/api/viewsets
diff options
context:
space:
mode:
authorGravatar mbaruh <[email protected]>2022-07-15 13:06:38 +0300
committerGravatar mbaruh <[email protected]>2022-07-15 13:33:21 +0300
commita77f35b598b227d87db0955a0b2bc97839dcb978 (patch)
tree5c2759a37f8dad14d9970e432b698326a86db661 /pydis_site/apps/api/viewsets
parentAdd UniqueConstraint to the Filter model (diff)
parentMerge pull request #740 from python-discord/update-django (diff)
Merge branch 'main' into new-filter-schema
Diffstat (limited to 'pydis_site/apps/api/viewsets')
-rw-r--r--pydis_site/apps/api/viewsets/__init__.py4
-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/api/viewsets/bot/infraction.py43
-rw-r--r--pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py3
-rw-r--r--pydis_site/apps/api/viewsets/bot/user.py19
8 files changed, 266 insertions, 16 deletions
diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py
index 4cf4c655..1dae9be1 100644
--- a/pydis_site/apps/api/viewsets/__init__.py
+++ b/pydis_site/apps/api/viewsets/__init__.py
@@ -1,13 +1,17 @@
# flake8: noqa
from .bot import (
BotSettingViewSet,
+ BumpedThreadViewSet,
DeletedMessageViewSet,
DocumentationLinkViewSet,
+ FilterListViewSet,
InfractionViewSet,
FilterListViewSet,
FilterViewSet,
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 4649fcde..33b65009 100644
--- a/pydis_site/apps/api/viewsets/bot/__init__.py
+++ b/pydis_site/apps/api/viewsets/bot/__init__.py
@@ -4,12 +4,15 @@ from .filters import (
FilterViewSet
)
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/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py
index 8a48ed1f..7f31292f 100644
--- a/pydis_site/apps/api/viewsets/bot/infraction.py
+++ b/pydis_site/apps/api/viewsets/bot/infraction.py
@@ -1,7 +1,9 @@
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
@@ -183,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']:
@@ -271,3 +277,28 @@ class InfractionViewSet(
"""
self.serializer_class = ExpandedInfractionSerializer
return self.partial_update(*args, **kwargs)
+
+ def create(self, request: HttpRequest, *args, **kwargs) -> Response:
+ """
+ Create an infraction for a target user.
+
+ Called by the Django Rest Framework in response to the corresponding HTTP request.
+ """
+ try:
+ return super().create(request, *args, **kwargs)
+ except IntegrityError as err:
+ # We need to use `__cause__` here, as Django reraises the internal
+ # UniqueViolation emitted by psycopg2 (which contains the attribute
+ # that we actually need)
+ #
+ # _meta is documented and mainly named that way to prevent
+ # name clashes: https://docs.djangoproject.com/en/dev/ref/models/meta/
+ if err.__cause__.diag.constraint_name == Infraction._meta.constraints[0].name:
+ raise ValidationError(
+ {
+ 'non_field_errors': [
+ 'This user already has an active infraction of this type.',
+ ]
+ }
+ )
+ raise # pragma: no cover - no other constraint to test with
diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
index 78f8c340..d0519e86 100644
--- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
+++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py
@@ -108,7 +108,7 @@ class OffTopicChannelNameViewSet(ModelViewSet):
'random_items': ["Must be a positive integer."]
})
- queryset = self.queryset.order_by('used', '?')[:random_count]
+ queryset = self.queryset.filter(active=True).order_by('used', '?')[:random_count]
# When any name is used in our listing then this means we reached end of round
# and we need to reset all other names `used` to False
@@ -133,7 +133,6 @@ class OffTopicChannelNameViewSet(ModelViewSet):
return Response(serialized.data)
params = {}
-
if active_param := request.query_params.get("active"):
params["active"] = active_param.lower() == "true"
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
index 1a5e79f8..3318b2b9 100644
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -1,7 +1,8 @@
import typing
from collections import OrderedDict
-from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Q
+from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.pagination import PageNumberPagination
@@ -77,6 +78,8 @@ class UserViewSet(ModelViewSet):
... }
#### Optional Query Parameters
+ - username: username to search for
+ - discriminator: discriminator to search for
- page_size: number of Users in one page, defaults to 10,000
- page: page number
@@ -233,6 +236,8 @@ class UserViewSet(ModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all().order_by("id")
pagination_class = UserListPagination
+ filter_backends = (DjangoFilterBackend,)
+ filter_fields = ('name', 'discriminator')
def get_serializer(self, *args, **kwargs) -> ModelSerializer:
"""Set Serializer many attribute to True if request body contains a list."""
@@ -261,12 +266,10 @@ class UserViewSet(ModelViewSet):
"""Request handler for metricity_data endpoint."""
user = self.get_object()
- try:
- Infraction.objects.get(user__id=user.id, active=True, type="voice_ban")
- except ObjectDoesNotExist:
- voice_banned = False
- else:
- voice_banned = True
+ has_voice_infraction = Infraction.objects.filter(
+ Q(user__id=user.id, active=True),
+ Q(type="voice_ban") | Q(type="voice_mute")
+ ).exists()
with Metricity() as metricity:
try:
@@ -275,7 +278,7 @@ class UserViewSet(ModelViewSet):
data["total_messages"] = metricity.total_messages(user.id)
data["activity_blocks"] = metricity.total_message_blocks(user.id)
- data["voice_banned"] = voice_banned
+ data["voice_gate_blocked"] = has_voice_infraction
return Response(data, status=status.HTTP_200_OK)
except NotFoundError:
return Response(dict(detail="User not found in metricity"),