From 9b0eeff865bb39454f201eb82b460fdc27899a90 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 29 Nov 2018 12:27:07 -0800 Subject: Django - Add Infractions API (#149) * add Infraction model and serialiser The model in not finalised. * fix mix up of serialiser fields * remove explicit id field and add foreign keys * remove unused import * disallow null for user * add view set and route * fix model and create migration * fix typo choice => choices * specify names for reverse accessors for User FKs * add django-filter * add filters to view set * add string dunder method to model * add list/retrieve tests * make reason nullable * add creation tests * remove support for PUT and DELETE * add support for PATCH * assert timestamps using strings rather than datetimes This is done to keep 3.6 support; datetime.fromisoformat() is 3.7+ * assert inserted_at * add unauthenticated tests * add bad value tests for list filters and retrieve * remove prefetch cache invalidation * make __str__ more descriptive * add field validation & remove note type * add tests for field validation * fix coverage for Infraction string dunder test * fix coverage (for sure this time) * return 400 for partial updates with frozen fields * add expanded serialiser and endpoints * test expanded endpoints * remove extra retrieve call * remove unnecessary try-finally blocks * remove extra blank line * document endpoints (except expanded) * document expanded routes * fix wrong routes in docstring (/infraction -> /infractions) * make merge migration --- api/viewsets.py | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 4 deletions(-) (limited to 'api/viewsets.py') diff --git a/api/viewsets.py b/api/viewsets.py index de5ddaf6..2784bbad 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -1,5 +1,8 @@ from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ParseError +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.decorators import action +from rest_framework.exceptions import ParseError, ValidationError +from rest_framework.filters import SearchFilter from rest_framework.mixins import ( CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin @@ -10,15 +13,15 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet from rest_framework_bulk import BulkCreateModelMixin from .models import ( - DocumentationLink, + DocumentationLink, Infraction, OffTopicChannelName, SnakeFact, SnakeIdiom, SnakeName, SpecialSnake, Tag, User ) from .serializers import ( - DocumentationLinkSerializer, - OffTopicChannelNameSerializer, + DocumentationLinkSerializer, ExpandedInfractionSerializer, + InfractionSerializer, OffTopicChannelNameSerializer, SnakeFactSerializer, SnakeIdiomSerializer, SnakeNameSerializer, SpecialSnakeSerializer, TagSerializer, UserSerializer @@ -89,6 +92,144 @@ class DocumentationLinkViewSet( lookup_field = 'package' +class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): + """ + View providing CRUD operations on infractions for Discord users. + + ## Routes + ### GET /bot/infractions + Retrieve all infractions. + May be filtered by the query parameters. + + #### Query parameters + - **active** `bool`: whether the infraction is still active + - **actor** `int`: snowflake of the user which applied the infraction + - **hidden** `bool`: whether the infraction is a shadow infraction + - **search** `str`: regular expression applied to the infraction's reason + - **type** `str`: the type of the infraction + - **user** `int`: snowflake of the user to which the infraction was applied + + Invalid query parameters are ignored. + + #### Response format + >>> [ + ... { + ... 'id': 5, + ... 'inserted_at': '2018-11-22T07:24:06.132307Z', + ... 'expires_at': '5018-11-20T15:52:00Z', + ... 'active': False, + ... 'user': 172395097705414656, + ... 'actor': 125435062127820800, + ... 'type': 'ban', + ... 'reason': 'He terk my jerb!', + ... 'hidden': True + ... } + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/infractions/ + Retrieve a single infraction by ID. + + #### Response format + See `GET /bot/infractions`. + + #### Status codes + - 200: returned on success + - 404: if an infraction with the given `id` could not be found + + ### POST /bot/infractions + Create a new infraction and return the created infraction. + Only `actor`, `type`, and `user` are required. + The `actor` and `user` must be users known by the site. + + #### Request body + >>> { + ... 'active': False, + ... 'actor': 125435062127820800, + ... 'expires_at': '5018-11-20T15:52:00+00:00', + ... 'hidden': True, + ... 'type': 'ban', + ... 'reason': 'He terk my jerb!', + ... 'user': 172395097705414656 + ... } + + #### Response format + See `GET /bot/infractions`. + + #### Status codes + - 201: returned on success + - 400: if a given user is unknown or a field in the request body is invalid + + ### PATCH /bot/infractions/ + Update the infraction with the given `id` and return the updated infraction. + Only `active`, `reason`, and `expires_at` may be updated. + + #### Request body + >>> { + ... 'active': True, + ... 'expires_at': '4143-02-15T21:04:31+00:00', + ... 'reason': 'durka derr' + ... } + + #### Response format + See `GET /bot/infractions`. + + #### Status codes + - 200: returned on success + - 400: if a field in the request body is invalid or disallowed + - 404: if an infraction with the given `id` could not be found + + ### Expanded routes + All routes support expansion of `user` and `actor` in responses. To use an expanded route, + append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`. + + #### Response format + See `GET /bot/users/` for the expanded formats of `user` and `actor`. Responses + are otherwise identical to their non-expanded counterparts. + """ + + serializer_class = InfractionSerializer + queryset = Infraction.objects.all() + filter_backends = (DjangoFilterBackend, SearchFilter) + filter_fields = ('user__id', 'actor__id', 'active', 'hidden', 'type') + search_fields = ('$reason',) + frozen_fields = ('id', 'inserted_at', 'type', 'user', 'actor', 'hidden') + + def partial_update(self, request, *args, **kwargs): + for field in request.data: + if field in self.frozen_fields: + raise ValidationError({field: ['This field cannot be updated.']}) + + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data) + + @action(url_path='expanded', detail=False) + def list_expanded(self, *args, **kwargs): + self.serializer_class = ExpandedInfractionSerializer + return self.list(*args, **kwargs) + + @list_expanded.mapping.post + def create_expanded(self, *args, **kwargs): + self.serializer_class = ExpandedInfractionSerializer + return self.create(*args, **kwargs) + + @action(url_path='expanded', url_name='detail-expanded', detail=True) + def retrieve_expanded(self, *args, **kwargs): + self.serializer_class = ExpandedInfractionSerializer + return self.retrieve(*args, **kwargs) + + @retrieve_expanded.mapping.patch + def partial_update_expanded(self, *args, **kwargs): + self.serializer_class = ExpandedInfractionSerializer + return self.partial_update(*args, **kwargs) + + class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): """ View of off-topic channel names used by the bot -- cgit v1.2.3 From 1435d100286c0ea434c2995d1cd993045b2103f0 Mon Sep 17 00:00:00 2001 From: ImportErr Date: Fri, 30 Nov 2018 18:48:32 +0000 Subject: Fixed member route typos --- api/viewsets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'api/viewsets.py') diff --git a/api/viewsets.py b/api/viewsets.py index de5ddaf6..86ab5758 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -365,7 +365,7 @@ class TagViewSet(ModelViewSet): - 201: returned on success - 400: if one of the given fields is invalid - ### PUT /bot/members/ + ### PUT /bot/tags/ Update the tag with the given `title`. #### Request body @@ -383,7 +383,7 @@ class TagViewSet(ModelViewSet): - 400: if the request body was invalid, see response body for details - 404: if the tag with the given `title` could not be found - ### PATCH /bot/members/ + ### PATCH /bot/tags/ Update the tag with the given `title`. #### Request body @@ -401,7 +401,7 @@ class TagViewSet(ModelViewSet): - 400: if the request body was invalid, see response body for details - 404: if the tag with the given `title` could not be found - ### DELETE /bot/members/ + ### DELETE /bot/tags/ Deletes the tag with the given `title`. #### Status codes -- cgit v1.2.3 From 8caf2e65fb084abbcede29cc977bdd6c5a6d0d9f Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Tue, 1 Jan 2019 20:52:45 +0100 Subject: apply stash --- api/urls.py | 14 +++++--- api/viewsets.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 13 deletions(-) (limited to 'api/viewsets.py') diff --git a/api/urls.py b/api/urls.py index 8229b08c..203b6b00 100644 --- a/api/urls.py +++ b/api/urls.py @@ -3,11 +3,11 @@ from rest_framework.routers import DefaultRouter from .views import HealthcheckView from .viewsets import ( - DocumentationLinkViewSet, MemberViewSet, - OffTopicChannelNameViewSet, - SnakeFactViewSet, SnakeIdiomViewSet, - SnakeNameViewSet, SpecialSnakeViewSet, - TagViewSet + DeletedMessageViewSet, DocumentationLinkViewSet, + MemberViewSet, OffTopicChannelNameViewSet, + RoleViewSet, SnakeFactViewSet, + SnakeIdiomViewSet, SnakeNameViewSet, + SpecialSnakeViewSet, TagViewSet ) @@ -26,6 +26,10 @@ bot_router.register( 'members', MemberViewSet ) +bot_router.register( + 'roles', + RoleViewSet +) bot_router.register( 'snake-facts', SnakeFactViewSet diff --git a/api/viewsets.py b/api/viewsets.py index 08660810..67e89ea6 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -12,16 +12,16 @@ from rest_framework_bulk import BulkCreateModelMixin from .models import ( DocumentationLink, Member, OffTopicChannelName, - SnakeFact, SnakeIdiom, - SnakeName, SpecialSnake, - Tag + Role, SnakeFact, + SnakeIdiom, SnakeName, + SpecialSnake, Tag ) from .serializers import ( - DocumentationLinkSerializer, - MemberSerializer, OffTopicChannelNameSerializer, - SnakeFactSerializer, SnakeIdiomSerializer, - SnakeNameSerializer, SpecialSnakeSerializer, - TagSerializer + DocumentationLinkSerializer, MemberSerializer, + OffTopicChannelNameSerializer, + RoleSerializer, SnakeFactSerializer, + SnakeIdiomSerializer, SnakeNameSerializer, + SpecialSnakeSerializer, TagSerializer ) @@ -178,6 +178,97 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): return Response(serialized.data) +class RoleViewSet(ModelViewSet): + """ + View providing CRUD access to the roles on our server, used + by the bot to keep a mirror of our server's roles on the site. + + ## Routes + ### GET /bot/roles + Returns all roles in the database. + + #### Response format + >>> [ + ... { + ... 'id': 267628507062992896, + ... 'name': "Admins", + ... 'colour': 1337, + ... 'permissions': 8 + ... } + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/roles/ + Gets a single role by ID. + + #### Response format + >>> { + ... 'id': 267628507062992896, + ... 'name': "Admins", + ... 'colour': 1337, + ... 'permissions': 8 + ... } + + #### Status codes + - 200: returned on success + - 404: if a role with the given `snowflake` could not be found + + ### POST /bot/roles + Adds a single new role. + + #### Request body + >>> { + ... 'id': int, + ... 'name': str, + ... 'colour': int, + ... 'permissions': int, + ... } + + #### Status codes + - 201: returned on success + - 400: if the body format is invalid + + ### PUT /bot/roles/ + Update the role with the given `snowflake`. + All fields in the request body are required. + + #### Request body + >>> { + ... 'id': int, + ... 'name': str, + ... 'colour': int, + ... 'permissions': int + ... } + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid + + ### PATCH /bot/roles/ + Update the role with the given `snowflake`. + All fields in the request body are required. + + >>> { + ... 'id': int, + ... 'name': str, + ... 'colour': int, + ... 'permissions': int + ... } + + ### DELETE /bot/roles/ + Deletes the role with the given `snowflake`. + + #### Status codes + - 204: returned on success + - 404: if a role with the given `snowflake` does not exist + """ + + queryset = Role.objects.all() + serializer = RoleSerializer + + class SnakeFactViewSet(ListModelMixin, GenericViewSet): """ View providing snake facts created by the Pydis community in the first code jam. -- cgit v1.2.3