diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/serializers.py | 20 | ||||
-rw-r--r-- | api/tests/test_members.py | 118 | ||||
-rw-r--r-- | api/urls.py | 3 | ||||
-rw-r--r-- | api/viewsets.py | 128 |
4 files changed, 258 insertions, 11 deletions
diff --git a/api/serializers.py b/api/serializers.py index 7a3a95e3..dc4d4a78 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,6 +1,7 @@ -from rest_framework.serializers import ModelSerializer +from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField +from rest_framework_bulk import BulkSerializerMixin -from .models import DocumentationLink, OffTopicChannelName, SnakeName +from .models import DocumentationLink, Member, OffTopicChannelName, Role, SnakeName class DocumentationLinkSerializer(ModelSerializer): @@ -22,3 +23,18 @@ class SnakeNameSerializer(ModelSerializer): class Meta: model = SnakeName fields = ('name', 'scientific') + + +class RoleSerializer(ModelSerializer): + class Meta: + model = Role + fields = ('id', 'name', 'colour', 'permissions') + + +class MemberSerializer(BulkSerializerMixin, ModelSerializer): + roles = PrimaryKeyRelatedField(many=True, queryset=Role.objects.all()) + + class Meta: + model = Member + fields = ('id', 'avatar_hash', 'name', 'discriminator', 'roles') + depth = 1 diff --git a/api/tests/test_members.py b/api/tests/test_members.py new file mode 100644 index 00000000..60ad8460 --- /dev/null +++ b/api/tests/test_members.py @@ -0,0 +1,118 @@ +from django.test import TestCase +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..models import Member, Role + + +class UnauthedDocumentationLinkAPITests(APISubdomainTestCase): + def setUp(self): + super().setUp() + self.client.force_authenticate(user=None) + + def test_detail_lookup_returns_401(self): + url = reverse('bot:member-detail', args=('whatever',), host='api') + response = self.client.get(url) + + self.assertEqual(response.status_code, 401) + + def test_list_returns_401(self): + url = reverse('bot:member-list', host='api') + response = self.client.get(url) + + self.assertEqual(response.status_code, 401) + + def test_create_returns_401(self): + url = reverse('bot:member-list', host='api') + response = self.client.post(url, data={'hi': 'there'}) + + self.assertEqual(response.status_code, 401) + + def test_delete_returns_401(self): + url = reverse('bot:member-detail', args=('whatever',), host='api') + response = self.client.delete(url) + + self.assertEqual(response.status_code, 401) + + +class CreationTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.role = Role.objects.create( + id=5, + name="Test role pls ignore", + colour=2, + permissions=0b01010010101 + ) + + def test_accepts_valid_data(self): + url = reverse('bot:member-list', host='api') + data = { + 'id': 42, + 'avatar_hash': "validavatarhashiswear", + 'name': "Test", + 'discriminator': 42, + 'roles': [ + self.role.id + ] + } + + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json(), data) + + user = Member.objects.get(id=42) + self.assertEqual(user.avatar_hash, data['avatar_hash']) + self.assertEqual(user.name, data['name']) + self.assertEqual(user.discriminator, data['discriminator']) + + def test_supports_multi_creation(self): + url = reverse('bot:member-list', host='api') + data = [ + { + 'id': 5, + 'avatar_hash': "hahayes", + 'name': "test man", + 'discriminator': 42, + 'roles': [ + self.role.id + ] + }, + { + 'id': 8, + 'avatar_hash': "maybenot", + 'name': "another test man", + 'discriminator': 555, + 'roles': [] + } + ] + + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json(), data) + + def test_returns_400_for_unknown_role_id(self): + url = reverse('bot:member-list', host='api') + data = { + 'id': 5, + 'avatar_hash': "hahayes", + 'name': "test man", + 'discriminator': 42, + 'roles': [ + 190810291 + ] + } + + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_returns_400_for_bad_data(self): + url = reverse('bot:member-list', host='api') + data = { + 'id': True, + 'avatar_hash': 1902831, + 'discriminator': "totally!" + } + + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) diff --git a/api/urls.py b/api/urls.py index 121e941c..dd7cdfce 100644 --- a/api/urls.py +++ b/api/urls.py @@ -2,13 +2,14 @@ from django.urls import include, path from rest_framework.routers import SimpleRouter from .views import HealthcheckView -from .viewsets import DocumentationLinkViewSet, OffTopicChannelNameViewSet, SnakeNameViewSet +from .viewsets import DocumentationLinkViewSet, OffTopicChannelNameViewSet, MemberViewSet, SnakeNameViewSet # http://www.django-rest-framework.org/api-guide/routers/#simplerouter bot_router = SimpleRouter(trailing_slash=False) bot_router.register('documentation-links', DocumentationLinkViewSet) bot_router.register('off-topic-channel-names', OffTopicChannelNameViewSet, base_name='offtopicchannelname') +bot_router.register('members', MemberViewSet) bot_router.register('snake-names', SnakeNameViewSet, base_name='snakename') diff --git a/api/viewsets.py b/api/viewsets.py index 82f8aea9..f3d530f0 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -3,10 +3,11 @@ from rest_framework.exceptions import ParseError from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin from rest_framework.response import Response from rest_framework.status import HTTP_201_CREATED -from rest_framework.viewsets import GenericViewSet, ViewSet +from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet +from rest_framework_bulk import BulkCreateModelMixin -from .models import DocumentationLink, OffTopicChannelName, SnakeName -from .serializers import DocumentationLinkSerializer, OffTopicChannelNameSerializer, SnakeNameSerializer +from .models import DocumentationLink, OffTopicChannelName, Member, SnakeName +from .serializers import DocumentationLinkSerializer, OffTopicChannelNameSerializer, MemberSerializer, SnakeNameSerializer class DocumentationLinkViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet): @@ -119,11 +120,6 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): def get_object(self): queryset = self.get_queryset() - if self.lookup_field not in self.kwargs: - raise ParseError(detail={ - 'name': ["This query parameter is required."] - }) - name = self.kwargs[self.lookup_field] return get_object_or_404(queryset, name=name) @@ -219,3 +215,119 @@ class SnakeNameViewSet(ViewSet): return Response(body) return Response({}) + + +class MemberViewSet(BulkCreateModelMixin, ModelViewSet): + """ + View providing CRUD operations on our Discord server's members through the bot. + + ## Routes + ### GET /bot/members + Returns all members currently known. + + #### Response format + >>> [ + ... { + ... 'id': 409107086526644234, + ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", + ... 'name': "Python", + ... 'discriminator': 4329, + ... 'roles': [ + ... 352427296948486144, + ... 270988689419665409, + ... 277546923144249364, + ... 458226699344019457 + ... ] + ... } + ... ] + + #### Status codes + - 200: returned on success + + ### GET /bot/members/<snowflake:int> + Gets a single member by ID. + + #### Response format + >>> { + ... 'id': 409107086526644234, + ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", + ... 'name': "Python", + ... 'discriminator': 4329, + ... 'roles': [ + ... 352427296948486144, + ... 270988689419665409, + ... 277546923144249364, + ... 458226699344019457 + ... ] + ... } + + #### Status codes + - 200: returned on success + - 404: if a member with the given `snowflake` could not be found + + ### POST /bot/members + Adds a single or multiple new members. + The roles attached to the member(s) must be roles known by the site. + + #### Request body + >>> { + ... 'id': int, + ... 'avatar': str, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int] + ... } + + Alternatively, request members can be POSTed as a list of above objects, + in which case multiple members will be created at once. + + #### Status codes + - 201: returned on success + - 400: if one of the given roles does not exist, or one of the given fields is invalid + + ### PUT /bot/members/<snowflake:int> + Update the member with the given `snowflake`. + All fields in the request body are required. + + #### Request body + >>> { + ... 'id': int, + ... 'avatar': str, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int] + ... } + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid, see response body for details + - 404: if the member with the given `snowflake` could not be found + + ### PATCH /bot/members/<snowflake:int> + Update the member with the given `snowflake`. + All fields in the request body are optional. + + #### Request body + >>> { + ... 'id': int, + ... 'avatar': str, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int] + ... } + + #### Status codes + - 200: returned on success + - 400: if the request body was invalid, see response body for details + - 404: if the member with the given `snowflake` could not be found + + ### DELETE /bot/members/<snowflake:int> + Deletes the member with the given `snowflake`. + + #### Status codes + - 204: returned on success + - 404: if a member with the given `snowflake` does not exist + """ + + serializer_class = MemberSerializer + queryset = Member.objects.all() |