diff options
Diffstat (limited to '')
| -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()  |