diff options
author | 2019-08-23 15:00:15 +0200 | |
---|---|---|
committer | 2019-08-23 15:00:15 +0200 | |
commit | f6c90633bc415aa1a1e625fb078212e9e015ba37 (patch) | |
tree | b211be1ec8e4b7ef38b824973716241e5bc6316b | |
parent | Properly handle `actor` via `PrimaryKeyRelatedField`. (diff) | |
parent | Making the comparison operators for Role act like those for d.py Role objects (diff) |
Merge pull request #236 from python-discord/django-roles-api-add-position
Add position field to Role model
-rw-r--r-- | pydis_site/apps/api/migrations/0039_add_position_field_to_role.py | 23 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/role.py | 22 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/user.py | 12 | ||||
-rw-r--r-- | pydis_site/apps/api/serializers.py | 2 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_models.py | 3 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_roles.py | 191 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_users.py | 57 | ||||
-rw-r--r-- | pydis_site/apps/api/viewsets/bot/role.py | 21 |
8 files changed, 321 insertions, 10 deletions
diff --git a/pydis_site/apps/api/migrations/0039_add_position_field_to_role.py b/pydis_site/apps/api/migrations/0039_add_position_field_to_role.py new file mode 100644 index 00000000..b6b27ff2 --- /dev/null +++ b/pydis_site/apps/api/migrations/0039_add_position_field_to_role.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.3 on 2019-08-15 11:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0038_merge_20190719_1817'), + ] + + operations = [ + migrations.AddField( + model_name='role', + name='position', + field=models.IntegerField(default=-1, help_text='The position of the role in the role hierarchy of the Discord Guild.'), + ), + migrations.AlterField( + model_name='role', + name='position', + field=models.IntegerField(help_text='The position of the role in the role hierarchy of the Discord Guild.'), + ) + ] diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index 34e74009..58bbf8b4 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -5,7 +7,12 @@ from pydis_site.apps.api.models.utils import ModelReprMixin class Role(ModelReprMixin, models.Model): - """A role on our Discord server.""" + """ + A role on our Discord server. + + The comparison operators <, <=, >, >=, ==, != act the same as they do with Role-objects of the + discord.py library, see https://discordpy.readthedocs.io/en/latest/api.html#discord.Role + """ id = models.BigIntegerField( primary_key=True, @@ -43,7 +50,18 @@ class Role(ModelReprMixin, models.Model): ), help_text="The integer value of the permission bitset of this role from Discord." ) + position = models.IntegerField( + help_text="The position of the role in the role hierarchy of the Discord Guild." + ) - def __str__(self): + def __str__(self) -> str: """Returns the name of the current role, for display purposes.""" return self.name + + def __lt__(self, other: Role) -> bool: + """Compares the roles based on their position in the role hierarchy of the guild.""" + return self.position < other.position + + def __le__(self, other: Role) -> bool: + """Compares the roles based on their position in the role hierarchy of the guild.""" + return self.position <= other.position diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index d4deb630..00c24d3d 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -51,3 +51,15 @@ class User(ModelReprMixin, models.Model): def __str__(self): """Returns the name and discriminator for the current user, for display purposes.""" return f"{self.name}#{self.discriminator}" + + @property + def top_role(self) -> Role: + """ + Attribute that returns the user's top role. + + This will fall back to the Developers role if the user does not have any roles. + """ + roles = self.roles.all() + if not roles: + return Role.objects.get(name="Developers") + return max(self.roles.all()) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 24533dd6..40ca755d 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -196,7 +196,7 @@ class RoleSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = Role - fields = ('id', 'name', 'colour', 'permissions') + fields = ('id', 'name', 'colour', 'permissions', 'position') class TagSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index cfc8464e..2120b056 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -51,7 +51,8 @@ class StringDunderMethodTests(SimpleTestCase): OffTopicChannelName(name='bob-the-builders-playground'), Role( id=5, name='test role', - colour=0x5, permissions=0 + colour=0x5, permissions=0, + position=10, ), Message( id=45, diff --git a/pydis_site/apps/api/tests/test_roles.py b/pydis_site/apps/api/tests/test_roles.py new file mode 100644 index 00000000..0a6cea9e --- /dev/null +++ b/pydis_site/apps/api/tests/test_roles.py @@ -0,0 +1,191 @@ +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..models import Role + + +class CreationTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + cls.admins_role = Role.objects.create( + id=1, + name="Admins", + colour=1, + permissions=1, + position=4, + ) + cls.developers_role = Role.objects.create( + id=4, + name="Helpers", + colour=4, + permissions=4, + position=1, + ) + cls.everyone_role = Role.objects.create( + id=5, + name="@everyone", + colour=5, + permissions=5, + position=0, + ) + cls.lowest_position_duplicate = Role.objects.create( + id=6, + name="lowest position duplicate", + colour=6, + permissions=6, + position=0, + ) + + def _validate_roledict(self, role_dict: dict) -> None: + """Helper method to validate a dict representing a role.""" + self.assertIsInstance(role_dict, dict) + self.assertEqual(len(role_dict), 5) + attributes = ('id', 'name', 'colour', 'permissions', 'position') + self.assertTrue(all(attribute in role_dict for attribute in attributes)) + + def test_role_ordering_lt(self): + """Tests the __lt__ comparisons based on role position in the hierarchy.""" + self.assertTrue(self.everyone_role < self.developers_role) + self.assertFalse(self.developers_role > self.admins_role) + + def test_role_ordering_le(self): + """Tests the __le__ comparisons based on role position in the hierarchy.""" + self.assertTrue(self.everyone_role <= self.developers_role) + self.assertTrue(self.everyone_role <= self.lowest_position_duplicate) + self.assertTrue(self.everyone_role >= self.lowest_position_duplicate) + self.assertTrue(self.developers_role >= self.everyone_role) + + self.assertFalse(self.developers_role >= self.admins_role) + self.assertFalse(self.developers_role <= self.everyone_role) + + def test_role_min_max_ordering(self): + """Tests the `min` and `max` operations based on the role hierarchy.""" + top_role_no_duplicates = max([self.developers_role, self.admins_role, self.everyone_role]) + self.assertIs(top_role_no_duplicates, self.admins_role) + + top_role_duplicates = max([self.developers_role, self.admins_role, self.admins_role]) + self.assertIs(top_role_duplicates, self.admins_role) + + bottom_role_no_duplicates = min( + [self.developers_role, self.admins_role, self.everyone_role] + ) + self.assertIs(bottom_role_no_duplicates, self.everyone_role) + + bottom_role_duplicates = min( + [self.lowest_position_duplicate, self.admins_role, self.everyone_role] + ) + self.assertIs(bottom_role_duplicates, self.lowest_position_duplicate) + + def test_role_list(self): + """Tests the GET list-view and validates the contents.""" + url = reverse('bot:role-list', host='api') + + response = self.client.get(url) + self.assertContains(response, text="id", count=4, status_code=200) + + roles = response.json() + self.assertIsInstance(roles, list) + self.assertEqual(len(roles), 4) + + for role in roles: + self._validate_roledict(role) + + def test_role_get_detail_success(self): + """Tests GET detail view of an existing role.""" + url = reverse('bot:role-detail', host='api', args=(self.admins_role.id, )) + response = self.client.get(url) + self.assertContains(response, text="id", count=1, status_code=200) + + role = response.json() + self._validate_roledict(role) + + admins_role = Role.objects.get(id=role["id"]) + self.assertEqual(admins_role.name, role["name"]) + self.assertEqual(admins_role.colour, role["colour"]) + self.assertEqual(admins_role.permissions, role["permissions"]) + self.assertEqual(admins_role.position, role["position"]) + + def test_role_post_201(self): + """Tests creation of a role with a valid request.""" + url = reverse('bot:role-list', host='api') + data = { + "id": 1234567890, + "name": "Role Creation Test", + "permissions": 0b01010010101, + "colour": 1, + "position": 10, + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + + def test_role_post_invalid_request_body(self): + """Tests creation of a role with an invalid request body.""" + url = reverse('bot:role-list', host='api') + data = { + "name": "Role Creation Test", + "permissions": 0b01010010101, + "colour": 1, + "position": 10, + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + self.assertJSONEqual(response.content, '{"id": ["This field is required."]}') + + def test_role_put_200(self): + """Tests PUT role request with valid request body.""" + url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) + data = { + "id": 123454321, + "name": "Role Put Alteration Test", + "permissions": 255, + "colour": 999, + "position": 20, + } + + response = self.client.put(url, data=data) + self.assertEqual(response.status_code, 200) + + admins_role = Role.objects.get(id=data["id"]) + self.assertEqual(admins_role.name, data["name"]) + self.assertEqual(admins_role.permissions, data["permissions"]) + self.assertEqual(admins_role.colour, data["colour"]) + self.assertEqual(admins_role.position, data["position"]) + + def test_role_put_invalid_request_body(self): + """Tests PUT role request with invalid request body.""" + url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) + data = { + "name": "Role Put Alteration Test", + "permissions": 255, + "colour": 999, + "position": 20, + } + response = self.client.put(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_role_patch_200(self): + """Tests PATCH role request with valid request body.""" + url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) + data = { + "name": "Owners" + } + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, 200) + + admins_role = Role.objects.get(id=self.admins_role.id) + self.assertEqual(admins_role.name, data["name"]) + + def test_role_delete_200(self): + """Tests DELETE requests for existing role.""" + url = reverse('bot:role-detail', host='api', args=(self.admins_role.id,)) + response = self.client.delete(url) + self.assertEqual(response.status_code, 204) + + def test_role_detail_404_all_methods(self): + """Tests detail view with non-existing ID.""" + url = reverse('bot:role-detail', host='api', args=(20190815,)) + + for method in ('get', 'put', 'patch', 'delete'): + response = getattr(self.client, method)(url) + self.assertEqual(response.status_code, 404) + self.assertJSONEqual(response.content, '{"detail": "Not found."}') diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 90bc3d30..bbdd3ff4 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -41,7 +41,8 @@ class CreationTests(APISubdomainTestCase): id=5, name="Test role pls ignore", colour=2, - permissions=0b01010010101 + permissions=0b01010010101, + position=1 ) def test_accepts_valid_data(self): @@ -119,3 +120,57 @@ class CreationTests(APISubdomainTestCase): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 400) + + +class UserModelTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): # noqa + cls.role_top = Role.objects.create( + id=777, + name="High test role", + colour=2, + permissions=0b01010010101, + position=10, + ) + cls.role_bottom = Role.objects.create( + id=888, + name="Low test role", + colour=2, + permissions=0b01010010101, + position=1, + ) + cls.developers_role = Role.objects.create( + id=1234567, + name="Developers", + colour=1234, + permissions=0b01010010101, + position=2, + ) + cls.user_with_roles = User.objects.create( + id=1, + avatar_hash="coolavatarhash", + name="Test User with two roles", + discriminator=1111, + in_guild=True, + ) + cls.user_with_roles.roles.add(cls.role_bottom, cls.role_top) + + cls.user_without_roles = User.objects.create( + id=2, + avatar_hash="coolavatarhash", + name="Test User without roles", + discriminator=2222, + in_guild=True, + ) + + def test_correct_top_role_property_user_with_roles(self): + """Tests if the top_role property returns the correct role.""" + top_role = self.user_with_roles.top_role + self.assertIsInstance(top_role, Role) + self.assertEqual(top_role.id, self.role_top.id) + + def test_correct_top_role_property_user_without_roles(self): + """Tests if the top_role property returns the correct role.""" + top_role = self.user_without_roles.top_role + self.assertIsInstance(top_role, Role) + self.assertEqual(top_role.id, self.developers_role.id) diff --git a/pydis_site/apps/api/viewsets/bot/role.py b/pydis_site/apps/api/viewsets/bot/role.py index 213f0a19..1f82208d 100644 --- a/pydis_site/apps/api/viewsets/bot/role.py +++ b/pydis_site/apps/api/viewsets/bot/role.py @@ -20,7 +20,8 @@ class RoleViewSet(ModelViewSet): ... 'id': 267628507062992896, ... 'name': "Admins", ... 'colour': 1337, - ... 'permissions': 8 + ... 'permissions': 8, + ... 'position': 1 ... } ... ] @@ -35,7 +36,8 @@ class RoleViewSet(ModelViewSet): ... 'id': 267628507062992896, ... 'name': "Admins", ... 'colour': 1337, - ... 'permissions': 8 + ... 'permissions': 8, + ... 'position': 1 ... } #### Status codes @@ -51,6 +53,7 @@ class RoleViewSet(ModelViewSet): ... 'name': str, ... 'colour': int, ... 'permissions': int, + ... 'position': 1, ... } #### Status codes @@ -66,24 +69,32 @@ class RoleViewSet(ModelViewSet): ... 'id': int, ... 'name': str, ... 'colour': int, - ... 'permissions': int + ... 'permissions': int, + ... 'position': 1, ... } #### Status codes - 200: returned on success - 400: if the request body was invalid + - 404: if a role with the given `snowflake` does not exist ### PATCH /bot/roles/<snowflake:int> 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 + ... 'permissions': int, + ... 'position': 1, ... } + #### Status codes + - 200: returned on success + - 400: if the request body was invalid + - 404: if a role with the given `snowflake` does not exist + ### DELETE /bot/roles/<snowflake:int> Deletes the role with the given `snowflake`. |