From c58f5c749ae1f4a411dd4cc9a8395dedddf93027 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 19 Nov 2018 13:17:36 -0800 Subject: Django - Add Support for Storing Users Not in Guild (#150) * rename Member to User * add boolean field to distinguish users in our server * mark roles as not required * fix import order lint errors * fix order of model registration --- api/admin.py | 6 +- api/migrations/0018_user_rename.py | 17 +++++ api/migrations/0019_user_in_guild.py | 18 ++++++ api/models.py | 8 ++- api/serializers.py | 12 ++-- api/tests/test_members.py | 117 --------------------------------- api/tests/test_models.py | 6 +- api/tests/test_users.py | 121 +++++++++++++++++++++++++++++++++++ api/urls.py | 8 +-- api/viewsets.py | 69 +++++++++++--------- 10 files changed, 215 insertions(+), 167 deletions(-) create mode 100644 api/migrations/0018_user_rename.py create mode 100644 api/migrations/0019_user_in_guild.py delete mode 100644 api/tests/test_members.py create mode 100644 api/tests/test_users.py (limited to 'api') diff --git a/api/admin.py b/api/admin.py index b06cc939..c98f24eb 100644 --- a/api/admin.py +++ b/api/admin.py @@ -1,16 +1,15 @@ from django.contrib import admin from .models import ( - DocumentationLink, Member, + DocumentationLink, OffTopicChannelName, Role, SnakeFact, SnakeIdiom, SnakeName, SpecialSnake, - Tag + Tag, User ) admin.site.register(DocumentationLink) -admin.site.register(Member) admin.site.register(OffTopicChannelName) admin.site.register(Role) admin.site.register(SnakeFact) @@ -18,3 +17,4 @@ admin.site.register(SnakeIdiom) admin.site.register(SnakeName) admin.site.register(SpecialSnake) admin.site.register(Tag) +admin.site.register(User) diff --git a/api/migrations/0018_user_rename.py b/api/migrations/0018_user_rename.py new file mode 100644 index 00000000..f88eb5bc --- /dev/null +++ b/api/migrations/0018_user_rename.py @@ -0,0 +1,17 @@ +# Generated by Django 2.1.3 on 2018-11-19 20:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0017_auto_20181029_1921'), + ] + + operations = [ + migrations.RenameModel( + old_name='Member', + new_name='User', + ), + ] diff --git a/api/migrations/0019_user_in_guild.py b/api/migrations/0019_user_in_guild.py new file mode 100644 index 00000000..fda008c4 --- /dev/null +++ b/api/migrations/0019_user_in_guild.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.3 on 2018-11-19 20:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0018_user_rename'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='in_guild', + field=models.BooleanField(default=True, help_text='Whether this user is in our server.'), + ), + ] diff --git a/api/models.py b/api/models.py index 9990e266..bcf4af78 100644 --- a/api/models.py +++ b/api/models.py @@ -167,8 +167,8 @@ class Role(ModelReprMixin, models.Model): return self.name -class Member(ModelReprMixin, models.Model): - """A member of our Discord server.""" +class User(ModelReprMixin, models.Model): + """A Discord user.""" id = models.BigIntegerField( # noqa primary_key=True, @@ -205,6 +205,10 @@ class Member(ModelReprMixin, models.Model): Role, help_text="Any roles this user has on our server." ) + in_guild = models.BooleanField( + default=True, + help_text="Whether this user is in our server." + ) def __str__(self): return f"{self.name}#{self.discriminator}" diff --git a/api/serializers.py b/api/serializers.py index f8d15bbf..ba6dfaaf 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -3,10 +3,10 @@ from rest_framework_bulk import BulkSerializerMixin from .models import ( DocumentationLink, - Member, OffTopicChannelName, + OffTopicChannelName, Role, SnakeFact, SnakeIdiom, SnakeName, - SpecialSnake, Tag + SpecialSnake, Tag, User ) @@ -61,10 +61,10 @@ class TagSerializer(ModelSerializer): fields = ('title', 'embed') -class MemberSerializer(BulkSerializerMixin, ModelSerializer): - roles = PrimaryKeyRelatedField(many=True, queryset=Role.objects.all()) +class UserSerializer(BulkSerializerMixin, ModelSerializer): + roles = PrimaryKeyRelatedField(many=True, queryset=Role.objects.all(), required=False) class Meta: - model = Member - fields = ('id', 'avatar_hash', 'name', 'discriminator', 'roles') + model = User + fields = ('id', 'avatar_hash', 'name', 'discriminator', 'roles', 'in_guild') depth = 1 diff --git a/api/tests/test_members.py b/api/tests/test_members.py deleted file mode 100644 index 47466b62..00000000 --- a/api/tests/test_members.py +++ /dev/null @@ -1,117 +0,0 @@ -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): # noqa - 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/tests/test_models.py b/api/tests/test_models.py index 91db2def..2e606801 100644 --- a/api/tests/test_models.py +++ b/api/tests/test_models.py @@ -1,11 +1,11 @@ from django.test import SimpleTestCase from ..models import ( - DocumentationLink, Member, ModelReprMixin, + DocumentationLink, ModelReprMixin, OffTopicChannelName, Role, SnakeFact, SnakeIdiom, SnakeName, SpecialSnake, - Tag + Tag, User ) @@ -41,7 +41,7 @@ class StringDunderMethodTests(SimpleTestCase): id=5, name='test role', colour=0x5, permissions=0 ), - Member( + User( id=5, name='bob', discriminator=1, avatar_hash=None ), diff --git a/api/tests/test_users.py b/api/tests/test_users.py new file mode 100644 index 00000000..8dadcbdb --- /dev/null +++ b/api/tests/test_users.py @@ -0,0 +1,121 @@ +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..models import Role, User + + +class UnauthedDocumentationLinkAPITests(APISubdomainTestCase): + def setUp(self): + super().setUp() + self.client.force_authenticate(user=None) + + def test_detail_lookup_returns_401(self): + url = reverse('bot:user-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:user-list', host='api') + response = self.client.get(url) + + self.assertEqual(response.status_code, 401) + + def test_create_returns_401(self): + url = reverse('bot:user-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:user-detail', args=('whatever',), host='api') + response = self.client.delete(url) + + self.assertEqual(response.status_code, 401) + + +class CreationTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + 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:user-list', host='api') + data = { + 'id': 42, + 'avatar_hash': "validavatarhashiswear", + 'name': "Test", + 'discriminator': 42, + 'roles': [ + self.role.id + ], + 'in_guild': True + } + + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json(), data) + + user = User.objects.get(id=42) + self.assertEqual(user.avatar_hash, data['avatar_hash']) + self.assertEqual(user.name, data['name']) + self.assertEqual(user.discriminator, data['discriminator']) + self.assertEqual(user.in_guild, data['in_guild']) + + def test_supports_multi_creation(self): + url = reverse('bot:user-list', host='api') + data = [ + { + 'id': 5, + 'avatar_hash': "hahayes", + 'name': "test man", + 'discriminator': 42, + 'roles': [ + self.role.id + ], + 'in_guild': True + }, + { + 'id': 8, + 'avatar_hash': "maybenot", + 'name': "another test man", + 'discriminator': 555, + 'roles': [], + 'in_guild': False + } + ] + + 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:user-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:user-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 8229b08c..59853934 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, + DocumentationLinkViewSet, OffTopicChannelNameViewSet, SnakeFactViewSet, SnakeIdiomViewSet, SnakeNameViewSet, SpecialSnakeViewSet, - TagViewSet + TagViewSet, UserViewSet ) @@ -23,8 +23,8 @@ bot_router.register( base_name='offtopicchannelname' ) bot_router.register( - 'members', - MemberViewSet + 'users', + UserViewSet ) bot_router.register( 'snake-facts', diff --git a/api/viewsets.py b/api/viewsets.py index 08660810..de5ddaf6 100644 --- a/api/viewsets.py +++ b/api/viewsets.py @@ -10,18 +10,18 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet from rest_framework_bulk import BulkCreateModelMixin from .models import ( - DocumentationLink, Member, + DocumentationLink, OffTopicChannelName, SnakeFact, SnakeIdiom, SnakeName, SpecialSnake, - Tag + Tag, User ) from .serializers import ( DocumentationLinkSerializer, - MemberSerializer, OffTopicChannelNameSerializer, + OffTopicChannelNameSerializer, SnakeFactSerializer, SnakeIdiomSerializer, SnakeNameSerializer, SpecialSnakeSerializer, - TagSerializer + TagSerializer, UserSerializer ) @@ -413,13 +413,13 @@ class TagViewSet(ModelViewSet): queryset = Tag.objects.all() -class MemberViewSet(BulkCreateModelMixin, ModelViewSet): +class UserViewSet(BulkCreateModelMixin, ModelViewSet): """ - View providing CRUD operations on our Discord server's members through the bot. + View providing CRUD operations on Discord users through the bot. ## Routes - ### GET /bot/members - Returns all members currently known. + ### GET /bot/users + Returns all users currently known. #### Response format >>> [ @@ -433,15 +433,16 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 270988689419665409, ... 277546923144249364, ... 458226699344019457 - ... ] + ... ], + ... 'in_guild': True ... } ... ] #### Status codes - 200: returned on success - ### GET /bot/members/ - Gets a single member by ID. + ### GET /bot/users/ + Gets a single user by ID. #### Response format >>> { @@ -454,16 +455,17 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 270988689419665409, ... 277546923144249364, ... 458226699344019457 - ... ] + ... ], + ... 'in_guild': True ... } #### Status codes - 200: returned on success - - 404: if a member with the given `snowflake` could not be found + - 404: if a user 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. + ### POST /bot/users + Adds a single or multiple new users. + The roles attached to the user(s) must be roles known by the site. #### Request body >>> { @@ -471,18 +473,19 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 'avatar': str, ... 'name': str, ... 'discriminator': int, - ... 'roles': List[int] + ... 'roles': List[int], + ... 'in_guild': bool ... } - Alternatively, request members can be POSTed as a list of above objects, - in which case multiple members will be created at once. + Alternatively, request users can be POSTed as a list of above objects, + in which case multiple users 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/ - Update the member with the given `snowflake`. + ### PUT /bot/users/ + Update the user with the given `snowflake`. All fields in the request body are required. #### Request body @@ -491,16 +494,17 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 'avatar': str, ... 'name': str, ... 'discriminator': int, - ... 'roles': List[int] + ... 'roles': List[int], + ... 'in_guild': bool ... } #### 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 + - 404: if the user with the given `snowflake` could not be found - ### PATCH /bot/members/ - Update the member with the given `snowflake`. + ### PATCH /bot/users/ + Update the user with the given `snowflake`. All fields in the request body are optional. #### Request body @@ -509,21 +513,22 @@ class MemberViewSet(BulkCreateModelMixin, ModelViewSet): ... 'avatar': str, ... 'name': str, ... 'discriminator': int, - ... 'roles': List[int] + ... 'roles': List[int], + ... 'in_guild': bool ... } #### 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 + - 404: if the user with the given `snowflake` could not be found - ### DELETE /bot/members/ - Deletes the member with the given `snowflake`. + ### DELETE /bot/users/ + Deletes the user with the given `snowflake`. #### Status codes - 204: returned on success - - 404: if a member with the given `snowflake` does not exist + - 404: if a user with the given `snowflake` does not exist """ - serializer_class = MemberSerializer - queryset = Member.objects.all() + serializer_class = UserSerializer + queryset = User.objects.all() -- cgit v1.2.3