diff options
| author | 2022-06-13 18:42:40 -0400 | |
|---|---|---|
| committer | 2022-06-13 18:42:40 -0400 | |
| commit | 8351bf5c98aca9c6147c69d4a8bfe1e6d920653d (patch) | |
| tree | f73baaa5f1020b8190a85033768f2c4ac2599ea1 /pydis_site/apps/api/viewsets/bot | |
| parent | Add PyCharm logo. (diff) | |
| parent | Merge pull request #699 from camcaswell/contrib-streamline (diff) | |
Merge branch 'main' into swfarnsworth/resources
Diffstat (limited to 'pydis_site/apps/api/viewsets/bot')
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/__init__.py | 3 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py | 73 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/aoc_link.py | 71 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/bumped_thread.py | 66 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/infraction.py | 43 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 3 | ||||
| -rw-r--r-- | pydis_site/apps/api/viewsets/bot/user.py | 5 | 
7 files changed, 256 insertions, 8 deletions
| diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index 84b87eab..262aa59f 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,12 +1,15 @@  # flake8: noqa  from .filter_list import FilterListViewSet  from .bot_setting import BotSettingViewSet +from .bumped_thread import BumpedThreadViewSet  from .deleted_message import DeletedMessageViewSet  from .documentation_link import DocumentationLinkViewSet  from .infraction import InfractionViewSet  from .nomination import NominationViewSet  from .off_topic_channel_name import OffTopicChannelNameViewSet  from .offensive_message import OffensiveMessageViewSet +from .aoc_link import AocAccountLinkViewSet +from .aoc_completionist_block import AocCompletionistBlockViewSet  from .reminder import ReminderViewSet  from .role import RoleViewSet  from .user import UserViewSet diff --git a/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py new file mode 100644 index 00000000..3a4cec60 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/aoc_completionist_block.py @@ -0,0 +1,73 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.mixins import ( +    CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot import AocCompletionistBlock +from pydis_site.apps.api.serializers import AocCompletionistBlockSerializer + + +class AocCompletionistBlockViewSet( +    GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin +): +    """ +    View providing management for Users blocked from gettign the AoC completionist Role. + +    ## Routes + +    ### GET /bot/aoc-completionist-blocks/ +    Returns all the AoC completionist blocks + +    #### Response format +    >>> [ +    ...     { +    ...         "user": 2, +    ...         "is_blocked": False, +    ...         "reason": "Too good to be true" +    ...     } +    ... ] + + +    ### GET /bot/aoc-completionist-blocks/<user__id:int> +    Retrieve a single Block by User ID + +    #### Response format +    >>> +    ...     { +    ...         "user": 2, +    ...         "is_blocked": False, +    ...         "reason": "Too good to be true" +    ...     } + +    #### Status codes +    - 200: returned on success +    - 404: returned if an AoC completionist block with the given `user__id` was not found. + +    ### POST /bot/aoc-completionist-blocks +    Adds a single AoC completionist block + +    #### Request body +    >>> { +    ...     "user": int, +    ...     "is_blocked": bool, +    ...     "reason": string +    ... } + +    #### Status codes +    - 204: returned on success +    - 400: if one of the given fields is invalid + +    ### DELETE /bot/aoc-completionist-blocks/<user__id:int> +    Deletes the AoC Completionist block item with the given `user__id`. + +    #### Status codes +    - 204: returned on success +    - 404: returned if the AoC Completionist block with the given `user__id` was not found + +    """ + +    serializer_class = AocCompletionistBlockSerializer +    queryset = AocCompletionistBlock.objects.all() +    filter_backends = (DjangoFilterBackend,) +    filter_fields = ("user__id", "is_blocked") diff --git a/pydis_site/apps/api/viewsets/bot/aoc_link.py b/pydis_site/apps/api/viewsets/bot/aoc_link.py new file mode 100644 index 00000000..c7a96629 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/aoc_link.py @@ -0,0 +1,71 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.mixins import ( +    CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin +) +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot import AocAccountLink +from pydis_site.apps.api.serializers import AocAccountLinkSerializer + + +class AocAccountLinkViewSet( +    GenericViewSet, CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListModelMixin +): +    """ +    View providing management for Users who linked their AoC accounts to their Discord Account. + +    ## Routes + +    ### GET /bot/aoc-account-links +    Returns all the AoC account links + +    #### Response format +    >>> [ +    ...     { +    ...         "user": 2, +    ...         "aoc_username": "AoCUser1" +    ...     }, +    ...     ... +    ... ] + + +    ### GET /bot/aoc-account-links/<user__id:int> +    Retrieve a AoC account link by User ID + +    #### Response format +    >>> +    ... { +    ...     "user": 2, +    ...     "aoc_username": "AoCUser1" +    ... } + +    #### Status codes +    - 200: returned on success +    - 404: returned if an AoC account link with the given `user__id` was not found. + +    ### POST /bot/aoc-account-links +    Adds a single AoC account link block + +    #### Request body +    >>> { +    ...     'user': int, +    ...     'aoc_username': str +    ... } + +    #### Status codes +    - 204: returned on success +    - 400: if one of the given fields was invalid + +    ### DELETE /bot/aoc-account-links/<user__id:int> +    Deletes the AoC account link item with the given `user__id`. + +    #### Status codes +    - 204: returned on success +    - 404: returned if the AoC account link with the given `user__id` was not found + +    """ + +    serializer_class = AocAccountLinkSerializer +    queryset = AocAccountLink.objects.all() +    filter_backends = (DjangoFilterBackend,) +    filter_fields = ("user__id", "aoc_username") diff --git a/pydis_site/apps/api/viewsets/bot/bumped_thread.py b/pydis_site/apps/api/viewsets/bot/bumped_thread.py new file mode 100644 index 00000000..9d77bb6b --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/bumped_thread.py @@ -0,0 +1,66 @@ +from rest_framework.mixins import ( +    CreateModelMixin, DestroyModelMixin, ListModelMixin +) +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.models.bot import BumpedThread +from pydis_site.apps.api.serializers import BumpedThreadSerializer + + +class BumpedThreadViewSet( +    GenericViewSet, CreateModelMixin, DestroyModelMixin, ListModelMixin +): +    """ +    View providing CRUD (Minus the U) operations on threads to be bumped. + +    ## Routes +    ### GET /bot/bumped-threads +    Returns all BumpedThread items in the database. + +    #### Response format +    >>> list[int] + +    #### Status codes +    - 200: returned on success +    - 401: returned if unauthenticated + +    ### GET /bot/bumped-threads/<thread_id:int> +    Returns whether a specific BumpedThread exists in the database. + +    #### Status codes +    - 204: returned on success +    - 404: returned if a BumpedThread with the given thread_id was not found. + +    ### POST /bot/bumped-threads +    Adds a single BumpedThread item to the database. + +    #### Request body +    >>> { +    ...    'thread_id': int, +    ... } + +    #### Status codes +    - 201: returned on success +    - 400: if one of the given fields is invalid + +    ### DELETE /bot/bumped-threads/<thread_id:int> +    Deletes the BumpedThread item with the given `thread_id`. + +    #### Status codes +    - 204: returned on success +    - 404: if a BumpedThread with the given `thread_id` does not exist +    """ + +    serializer_class = BumpedThreadSerializer +    queryset = BumpedThread.objects.all() + +    def retrieve(self, request: Request, *args, **kwargs) -> Response: +        """ +        DRF method for checking if the given BumpedThread exists. + +        Called by the Django Rest Framework in response to the corresponding HTTP request. +        """ +        self.get_object() +        return Response(status=204) diff --git a/pydis_site/apps/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 a867a80f..3318b2b9 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -2,6 +2,7 @@ import typing  from collections import OrderedDict  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.""" | 
