aboutsummaryrefslogtreecommitdiffstats
path: root/api
diff options
context:
space:
mode:
Diffstat (limited to 'api')
-rw-r--r--api/serializers.py20
-rw-r--r--api/tests/test_members.py118
-rw-r--r--api/urls.py3
-rw-r--r--api/viewsets.py128
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()