From a3d2938d63443ee6fa43751f42c935b22d4efb47 Mon Sep 17 00:00:00 2001 From: ks123 Date: Mon, 30 Mar 2020 17:28:44 +0300 Subject: (Off-topic Channel Names Viewset): Added documentation about new `mark_used` query parameter, added implementation of this param. --- .../api/viewsets/bot/off_topic_channel_name.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index d6da2399..4328c894 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -21,6 +21,10 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): If the `random_items` query parameter is given, for example using... $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 ... then the API will return `5` random items from the database. + If the `mark_used` query parameter is given like... + $ curl api.pydis.local:8000/bot/off-topic-channel-names?random_items=5&mark_used=true + ... then the API will mark returned `5` items `used`. + When running out of names, API will mark all names to not used and start new round. #### Response format Return a list of off-topic-channel names: @@ -106,6 +110,31 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) + if 'mark_used' in request.query_params and request.query_params['mark_used']: + queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] + self.get_queryset().filter( + name__in=(query.name for query in queryset) + ).update(used=True) + + # When client request more channel names than non-used names is available, start + # new round of names. + if len(queryset) < random_count: + # Get how much names still missing and don't fetch duplicate names. + need_more = random_count - len(queryset) + ext = self.get_queryset().order_by('?').exclude( + name__in=(query.name for query in queryset) + )[:need_more] + + # Set all names `used` field to False except these that we just used. + self.get_queryset().exclude(name__in=( + query.name for query in ext) + ).update(used=False) + # Join original queryset (that had missing names) + # and extension with these missing names. + queryset = list(queryset) + list(ext) + serialized = self.serializer_class(queryset, many=True) + return Response(serialized.data) + queryset = self.get_queryset().order_by('?')[:random_count] serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 7d5e1f9c60007b230fecdb2b649c5574462fdeb1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:06:21 +0300 Subject: OT: Refactor off-topic-names random items getting Remove `mark_used` parameter and move this functionality to `random_items` parameter. Update docstring of class --- .../api/viewsets/bot/off_topic_channel_name.py | 55 ++++++++++------------ 1 file changed, 25 insertions(+), 30 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 4328c894..e6cf8172 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -20,11 +20,9 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): Return all known off-topic channel names from the database. If the `random_items` query parameter is given, for example using... $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 - ... then the API will return `5` random items from the database. - If the `mark_used` query parameter is given like... - $ curl api.pydis.local:8000/bot/off-topic-channel-names?random_items=5&mark_used=true - ... then the API will mark returned `5` items `used`. - When running out of names, API will mark all names to not used and start new round. + ... then the API will return `5` random items from the database + that is not used in current rotation. + When running out of names, API will mark all names to not used and start new rotation. #### Response format Return a list of off-topic-channel names: @@ -110,32 +108,29 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - if 'mark_used' in request.query_params and request.query_params['mark_used']: - queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] - self.get_queryset().filter( + queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] + self.get_queryset().filter( + name__in=(query.name for query in queryset) + ).update(used=True) + + # When client request more channel names than non-used names is available, start + # new round of names. + if len(queryset) < random_count: + # Get how much names still missing and don't fetch duplicate names. + need_more = random_count - len(queryset) + ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) - ).update(used=True) - - # When client request more channel names than non-used names is available, start - # new round of names. - if len(queryset) < random_count: - # Get how much names still missing and don't fetch duplicate names. - need_more = random_count - len(queryset) - ext = self.get_queryset().order_by('?').exclude( - name__in=(query.name for query in queryset) - )[:need_more] - - # Set all names `used` field to False except these that we just used. - self.get_queryset().exclude(name__in=( - query.name for query in ext) - ).update(used=False) - # Join original queryset (that had missing names) - # and extension with these missing names. - queryset = list(queryset) + list(ext) - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) - - queryset = self.get_queryset().order_by('?')[:random_count] + )[:need_more] + + # Set all names `used` field to False except these that we just used. + self.get_queryset().exclude(name__in=( + query.name for query in ext) + ).update(used=False) + + # Join original queryset (that had missing names) + # and extension with these missing names. + queryset = list(queryset) + list(ext) + serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 5c53af373f9f868c5603a903a2c2d9636c2d0982 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:35:17 +0300 Subject: OT: Fix comments --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index e6cf8172..c4520a48 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -113,16 +113,16 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): name__in=(query.name for query in queryset) ).update(used=True) - # When client request more channel names than non-used names is available, start - # new round of names. + # When the client requests more channel names than are available, + # we reset all names to used=False and start a new round of names. if len(queryset) < random_count: - # Get how much names still missing and don't fetch duplicate names. + # Figure out how many additional names we need, and don't fetch duplicate names. need_more = random_count - len(queryset) ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) )[:need_more] - # Set all names `used` field to False except these that we just used. + # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( query.name for query in ext) ).update(used=False) -- cgit v1.2.3 From e046ac1d764d80a6a04c3c2d70fb8b4d718b6210 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:36:24 +0300 Subject: OT: Rename variable `need_more` to `names_needed` --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index c4520a48..1099922c 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -117,10 +117,10 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): # we reset all names to used=False and start a new round of names. if len(queryset) < random_count: # Figure out how many additional names we need, and don't fetch duplicate names. - need_more = random_count - len(queryset) + names_needed = random_count - len(queryset) ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) - )[:need_more] + )[:names_needed] # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( -- cgit v1.2.3 From 9cedba3870947ca10218c65c4106f5dd095d230c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:37:29 +0300 Subject: OT: Rename variable `ext` to `other_names` --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 1099922c..9af69ae4 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -118,18 +118,18 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): if len(queryset) < random_count: # Figure out how many additional names we need, and don't fetch duplicate names. names_needed = random_count - len(queryset) - ext = self.get_queryset().order_by('?').exclude( + other_names = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) )[:names_needed] # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( - query.name for query in ext) + query.name for query in other_names) ).update(used=False) # Join original queryset (that had missing names) # and extension with these missing names. - queryset = list(queryset) + list(ext) + queryset = list(queryset) + list(other_names) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 0ba6e25f88400e9a54ae9be2c176687ad69bb94f Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 20 Aug 2020 15:15:21 +0200 Subject: Allow direct fetching of reminders by id --- pydis_site/apps/api/tests/test_reminders.py | 28 ++++++++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/reminder.py | 8 +++++++- 2 files changed, 35 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index a05d9296..9dffb668 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -163,6 +163,34 @@ class ReminderListTests(APISubdomainTestCase): self.assertEqual(response.json(), [self.rem_dict_one]) +class ReminderRetrieveTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.author = User.objects.create( + id=6789, + name='Reminder author', + discriminator=6789, + ) + + cls.reminder = Reminder.objects.create( + author=cls.author, + content="Reminder content", + expiration=datetime.utcnow().isoformat(), + jump_url="http://example.com/", + channel_id=123 + ) + + def test_retrieve_unknown_returns_404(self): + url = reverse('bot:reminder-detail', args=("not_an_id",), host='api') + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_retrieve_known_returns_200(self): + url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + class ReminderUpdateTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 6f8a28f2..00413fb1 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -4,6 +4,7 @@ from rest_framework.mixins import ( CreateModelMixin, DestroyModelMixin, ListModelMixin, + RetrieveModelMixin, UpdateModelMixin ) from rest_framework.viewsets import GenericViewSet @@ -13,7 +14,12 @@ from pydis_site.apps.api.serializers import ReminderSerializer class ReminderViewSet( - CreateModelMixin, ListModelMixin, DestroyModelMixin, UpdateModelMixin, GenericViewSet + CreateModelMixin, + RetrieveModelMixin, + ListModelMixin, + DestroyModelMixin, + UpdateModelMixin, + GenericViewSet, ): """ View providing CRUD access to reminders. -- cgit v1.2.3 From 700de6b08b70f40dc97f3644d4e0ee32ab8f6ccf Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 20 Aug 2020 15:24:29 +0200 Subject: Update docstring for new fetching behaviour --- pydis_site/apps/api/viewsets/bot/reminder.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 00413fb1..111660d9 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -50,6 +50,30 @@ class ReminderViewSet( #### Status codes - 200: returned on success + ### GET /bot/reminders/ + Fetches the reminder with the given id. + + #### Response format + >>> + ... { + ... 'active': True, + ... 'author': 1020103901030, + ... 'mentions': [ + ... 336843820513755157, + ... 165023948638126080, + ... 267628507062992896 + ... ], + ... 'content': "Make dinner", + ... 'expiration': '5018-11-20T15:52:00Z', + ... 'id': 11, + ... 'channel_id': 634547009956872193, + ... 'jump_url': "https://discord.com/channels///" + ... } + + #### Status codes + - 200: returned on success + - 404: returned when the reminder doesn't exist + ### POST /bot/reminders Create a new reminder. -- cgit v1.2.3 From 464408462cdb737b1eb78777c356253f9d5f0a4a Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Wed, 26 Aug 2020 20:51:05 +0530 Subject: add pagination for GET request on /bot/users endpoint Pagination is done via PageNumberPagination, i.e, each page contains a specific number of `user` objects. --- pydis_site/apps/api/viewsets/bot/user.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 9571b3d7..b016bb66 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,3 +1,4 @@ +from rest_framework.pagination import PageNumberPagination from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin @@ -5,17 +6,28 @@ from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer +class UserListPagination(PageNumberPagination): + """Custom pagination class for the User Model.""" + + page_size = 10000 + page_size_query_param = "page_size" + + class UserViewSet(BulkCreateModelMixin, ModelViewSet): """ View providing CRUD operations on Discord users through the bot. ## Routes ### GET /bot/users - Returns all users currently known. + Returns all users currently known with pagination. #### Response format - >>> [ - ... { + >>> { + ... 'count': 95000, + ... 'next': "http://api.pythondiscord.com/bot/users?page=2", + ... 'previous': None, + ... 'results': [ + ... { ... 'id': 409107086526644234, ... 'name': "Python", ... 'discriminator': 4329, @@ -26,8 +38,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): ... 458226699344019457 ... ], ... 'in_guild': True - ... } - ... ] + ... }, + ... ] + ... } + + #### Query Parameters + - page_size: Number of Users in one page. + - page: Page number #### Status codes - 200: returned on success @@ -118,4 +135,5 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): """ serializer_class = UserSerializer - queryset = User.objects + queryset = User.objects.all() + pagination_class = UserListPagination -- cgit v1.2.3 From 8e636a54b449f44f5bff56577a05d9a6a2dd72c0 Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Wed, 26 Aug 2020 21:59:46 +0530 Subject: add support for bulk updates on user model implemented a method to handle bulk updates on user model via a new endpoint: /bot/users/bulk_patch --- pydis_site/apps/api/serializers.py | 77 +++++++++++++++++++++++++++++++- pydis_site/apps/api/viewsets/bot/user.py | 60 +++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 52e0d972..757faeae 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,5 +1,13 @@ """Converters from Django models to data interchange formats and back.""" -from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError +from django.db.models.query import QuerySet +from rest_framework.serializers import ( + ListSerializer, + ModelSerializer, + PrimaryKeyRelatedField, + ValidationError +) +from rest_framework.settings import api_settings +from rest_framework.utils import html from rest_framework.validators import UniqueTogetherValidator from rest_framework_bulk import BulkSerializerMixin @@ -260,6 +268,72 @@ class TagSerializer(ModelSerializer): fields = ('title', 'embed') +class UserListSerializer(ListSerializer): + """List serializer for User model to handle bulk updates.""" + + def to_internal_value(self, data: list) -> list: + """ + Overriding `to_internal_value` function with a few changes to support bulk updates. + + List of dicts of native values <- List of dicts of primitive datatypes. + """ + if html.is_html_input(data): + data = html.parse_html_list(data, default=[]) + + if not isinstance(data, list): + message = self.error_messages['not_a_list'].format( + input_type=type(data).__name__ + ) + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='not_a_list') + + if not self.allow_empty and len(data) == 0: + message = self.error_messages['empty'] + raise ValidationError({ + api_settings.NON_FIELD_ERRORS_KEY: [message] + }, code='empty') + + ret = [] + errors = [] + + for item in data: + # inserted code + # bug: https://github.com/miki725/django-rest-framework-bulk/issues/68 + # ----------------- + try: + self.child.instance = self.instance.get(id=item['id']) + except User.DoesNotExist: + self.child.instance = None + # ----------------- + self.child.initial_data = item + try: + validated = self.child.run_validation(item) + except ValidationError as exc: + errors.append(exc.detail) + else: + ret.append(validated) + errors.append({}) + + if any(errors): + raise ValidationError(errors) + + return ret + + def update(self, instance: QuerySet, validated_data: list) -> list: + """Override update method to support bulk updates.""" + instance_mapping = {user.id: user for user in instance} + data_mapping = {item['id']: item for item in validated_data} + + updated = [] + for book_id, data in data_mapping.items(): + book = instance_mapping.get(book_id, None) + if book is not None: + updated.append(self.child.update(book, data)) + + return updated + + class UserSerializer(BulkSerializerMixin, ModelSerializer): """A class providing (de-)serialization of `User` instances.""" @@ -269,6 +343,7 @@ class UserSerializer(BulkSerializerMixin, ModelSerializer): model = User fields = ('id', 'name', 'discriminator', 'roles', 'in_guild') depth = 1 + list_serializer_class = UserListSerializer class NominationSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index b016bb66..d64ca113 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,4 +1,8 @@ +from rest_framework import status +from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin @@ -137,3 +141,59 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): serializer_class = UserSerializer queryset = User.objects.all() pagination_class = UserListPagination + + @action(detail=False, methods=["PATCH"]) + def bulk_patch(self, request: Request) -> Response: + """ + Update multiple User objects in a single request. + + ## Route + ### PATCH /bot/users/bulk_patch + Update all users with the IDs. + `id` field is mandatory, rest are optional. + + #### Request body + >>> [ + ... { + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... }, + ... { + ... 'id': int, + ... 'name': str, + ... 'discriminator': 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. + """ + queryset = self.get_queryset() + try: + object_ids = [item["id"] for item in request.data] + except KeyError: + # user ID not provided in request body. + resp = { + "Error": "User ID not provided." + } + return Response(resp, status=status.HTTP_400_BAD_REQUEST) + + filtered_instances = queryset.filter(id__in=object_ids) + + serializer = self.get_serializer( + instance=filtered_instances, + data=request.data, + many=True, + partial=True + ) + + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -- cgit v1.2.3 From d52a20962bbda8f94646ff12c38eb71e2a5241ab Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:38:25 +0300 Subject: (Tag Cleanup): Removed Tags viewset. --- pydis_site/apps/api/viewsets/bot/__init__.py | 1 - pydis_site/apps/api/viewsets/bot/tag.py | 105 --------------------------- 2 files changed, 106 deletions(-) delete mode 100644 pydis_site/apps/api/viewsets/bot/tag.py (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index e64e3988..84b87eab 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -9,5 +9,4 @@ from .off_topic_channel_name import OffTopicChannelNameViewSet from .offensive_message import OffensiveMessageViewSet from .reminder import ReminderViewSet from .role import RoleViewSet -from .tag import TagViewSet from .user import UserViewSet diff --git a/pydis_site/apps/api/viewsets/bot/tag.py b/pydis_site/apps/api/viewsets/bot/tag.py deleted file mode 100644 index 7e9ba117..00000000 --- a/pydis_site/apps/api/viewsets/bot/tag.py +++ /dev/null @@ -1,105 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.tag import Tag -from pydis_site.apps.api.serializers import TagSerializer - - -class TagViewSet(ModelViewSet): - """ - View providing CRUD operations on tags shown by our bot. - - ## Routes - ### GET /bot/tags - Returns all tags in the database. - - #### Response format - >>> [ - ... { - ... 'title': "resources", - ... 'embed': { - ... 'content': "Did you really think I'd put something useful here?" - ... } - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/tags/ - Gets a single tag by its title. - - #### Response format - >>> { - ... 'title': "My awesome tag", - ... 'embed': { - ... 'content': "totally not filler words" - ... } - ... } - - #### Status codes - - 200: returned on success - - 404: if a tag with the given `title` could not be found - - ### POST /bot/tags - Adds a single tag to the database. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### PUT /bot/tags/ - Update the tag with the given `title`. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the tag with the given `title` could not be found - - ### PATCH /bot/tags/ - Update the tag with the given `title`. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the tag with the given `title` could not be found - - ### DELETE /bot/tags/ - Deletes the tag with the given `title`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `title` does not exist - """ - - serializer_class = TagSerializer - queryset = Tag.objects.all() -- cgit v1.2.3 From 567f7f0c4a71ace555c9b3123ef50d6ae47756cd Mon Sep 17 00:00:00 2001 From: rohanjnr Date: Fri, 28 Aug 2020 12:58:55 +0530 Subject: Add code to replace restframework_bulk package for bulk create and simplify UserListSerializer `to_internal_value()` function is not longer overriden in UserListSerializer, this is due to explicitly stating the `id` field in UserSerializer as mentioned in the documentation. Override `create()` method in UserListSerializer and override `get_serializer()` method in `UserViewSet` to support bulk creation. --- pydis_site/apps/api/serializers.py | 73 ++++++++------------------------ pydis_site/apps/api/viewsets/bot/user.py | 20 +++++++-- 2 files changed, 35 insertions(+), 58 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 0589ce77..21c488a8 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,15 +1,13 @@ """Converters from Django models to data interchange formats and back.""" from django.db.models.query import QuerySet from rest_framework.serializers import ( + IntegerField, ListSerializer, ModelSerializer, PrimaryKeyRelatedField, ValidationError ) -from rest_framework.settings import api_settings -from rest_framework.utils import html from rest_framework.validators import UniqueTogetherValidator -from rest_framework_bulk import BulkSerializerMixin from .models import ( BotSetting, @@ -271,55 +269,18 @@ class TagSerializer(ModelSerializer): class UserListSerializer(ListSerializer): """List serializer for User model to handle bulk updates.""" - def to_internal_value(self, data: list) -> list: - """ - Overriding `to_internal_value` function with a few changes to support bulk updates. - - ref: https://github.com/miki725/django-rest-framework-bulk/issues/68 + def create(self, validated_data: list) -> list: + """Override create method to optimize django queries.""" + present_users = User.objects.all() + new_users = [] + present_user_ids = [user.id for user in present_users] - List of dicts of native values <- List of dicts of primitive datatypes. - """ - if html.is_html_input(data): - data = html.parse_html_list(data, default=[]) + for user_dict in validated_data: + if user_dict["id"] in present_user_ids: + raise ValidationError({"id": "User already exists."}) + new_users.append(User(**user_dict)) - if not isinstance(data, list): - message = self.error_messages['not_a_list'].format( - input_type=type(data).__name__ - ) - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] - }, code='not_a_list') - - if not self.allow_empty and len(data) == 0: - message = self.error_messages['empty'] - raise ValidationError({ - api_settings.NON_FIELD_ERRORS_KEY: [message] - }, code='empty') - - ret = [] - errors = [] - - for item in data: - # inserted code - # ----------------- - try: - self.child.instance = self.instance.get(id=item['id']) - except (User.DoesNotExist, AttributeError): - self.child.instance = None - # ----------------- - self.child.initial_data = item - try: - validated = self.child.run_validation(item) - except ValidationError as exc: - errors.append(exc.detail) - else: - ret.append(validated) - errors.append({}) - - if any(errors): - raise ValidationError(errors) - - return ret + return User.objects.bulk_create(new_users) def update(self, instance: QuerySet, validated_data: list) -> list: """ @@ -331,17 +292,19 @@ class UserListSerializer(ListSerializer): data_mapping = {item['id']: item for item in validated_data} updated = [] - for book_id, data in data_mapping.items(): - book = instance_mapping.get(book_id, None) - if book is not None: - updated.append(self.child.update(book, data)) + for user_id, data in data_mapping.items(): + user = instance_mapping.get(user_id, None) + if user: + updated.append(self.child.update(user, data)) return updated -class UserSerializer(BulkSerializerMixin, ModelSerializer): +class UserSerializer(ModelSerializer): """A class providing (de-)serialization of `User` instances.""" + id = IntegerField(min_value=0) + class Meta: """Metadata defined for the Django REST Framework.""" diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index d64ca113..d015fe71 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -3,8 +3,8 @@ from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet -from rest_framework_bulk import BulkCreateModelMixin from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer @@ -17,7 +17,7 @@ class UserListPagination(PageNumberPagination): page_size_query_param = "page_size" -class UserViewSet(BulkCreateModelMixin, ModelViewSet): +class UserViewSet(ModelViewSet): """ View providing CRUD operations on Discord users through the bot. @@ -142,7 +142,14 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): queryset = User.objects.all() pagination_class = UserListPagination - @action(detail=False, methods=["PATCH"]) + def get_serializer(self, *args, **kwargs) -> ModelSerializer: + """Set Serializer many attribute to True if request body contains a list.""" + if isinstance(kwargs.get('data', {}), list): + kwargs['many'] = True + + return super().get_serializer(*args, **kwargs) + + @action(detail=False, methods=["PATCH"], name='user-bulk-patch') def bulk_patch(self, request: Request) -> Response: """ Update multiple User objects in a single request. @@ -186,6 +193,13 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): filtered_instances = queryset.filter(id__in=object_ids) + if filtered_instances.count() != len(object_ids): + # If all user objects passed in request.body are not present in the database. + resp = { + "Error": "User object not found." + } + return Response(resp, status=status.HTTP_404_NOT_FOUND) + serializer = self.get_serializer( instance=filtered_instances, data=request.data, -- cgit v1.2.3 From 32b76b192d72f4235a64b8024131988b4e4c0c36 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 08:55:57 +0300 Subject: Simplify non-random off-topic names selection --- .../api/viewsets/bot/off_topic_channel_name.py | 39 ++++++++++------------ 1 file changed, 17 insertions(+), 22 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 9af69ae4..29978015 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,3 +1,4 @@ +from django.db.models import Case, When, Value from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -108,28 +109,22 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] - self.get_queryset().filter( - name__in=(query.name for query in queryset) - ).update(used=True) - - # When the client requests more channel names than are available, - # we reset all names to used=False and start a new round of names. - if len(queryset) < random_count: - # Figure out how many additional names we need, and don't fetch duplicate names. - names_needed = random_count - len(queryset) - other_names = self.get_queryset().order_by('?').exclude( - name__in=(query.name for query in queryset) - )[:names_needed] - - # Reset the `used` field to False for all names except the ones we just used. - self.get_queryset().exclude(name__in=( - query.name for query in other_names) - ).update(used=False) - - # Join original queryset (that had missing names) - # and extension with these missing names. - queryset = list(queryset) + list(other_names) + queryset = self.get_queryset().order_by('used', '?')[:random_count] + + # When any name is used in our listing then this means we reached end of round + # and we need to reset all other names `used` to False + if any(offtopic_name.used for offtopic_name in queryset): + self.get_queryset().update( + used=Case( # These names that we just got have to be excluded from updating to False + When(name__in=(offtopic_name.name for offtopic_name in queryset), then=Value(True)), + default=Value(False) + ) + ) + else: + # Otherwise mark selected names `used` to True + self.get_queryset().filter( + name__in=(offtopic_name.name for offtopic_name in queryset) + ).update(used=True) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From ac7745e4ea79fc129447e9b472d3dbd49bf00fd6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 09:21:15 +0300 Subject: Fix linting issues on off-topic viewset --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 29978015..826ad25e 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,4 +1,4 @@ -from django.db.models import Case, When, Value +from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -114,9 +114,13 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): # When any name is used in our listing then this means we reached end of round # and we need to reset all other names `used` to False if any(offtopic_name.used for offtopic_name in queryset): + # These names that we just got have to be excluded from updating used to False self.get_queryset().update( - used=Case( # These names that we just got have to be excluded from updating to False - When(name__in=(offtopic_name.name for offtopic_name in queryset), then=Value(True)), + used=Case( + When( + name__in=(offtopic_name.name for offtopic_name in queryset), + then=Value(True) + ), default=Value(False) ) ) -- cgit v1.2.3 From dae68c46831ef14e6a0715add1025f9af1b5a73d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Aug 2020 10:00:45 -0700 Subject: Revert pull request #348 Revert commit 330a27926a7f2a9bdae56a5928cd81e55ccb38ed to reverse changes made to 6c08da2f3e48db9c9dd452f2e0505c4a61e46592. Migrations were failing when those changes were deployed. --- .../api/migrations/0051_create_news_setting.py | 25 ++++ .../api/migrations/0052_create_news_setting.py | 25 ---- .../api/migrations/0052_remove_user_avatar_hash.py | 17 +++ .../migrations/0053_offtopicchannelname_used.py | 18 --- .../api/migrations/0053_user_roles_to_array.py | 24 ++++ .../api/migrations/0054_remove_user_avatar_hash.py | 17 --- .../0054_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0055_merge_20200714_2027.py | 14 ++ .../apps/api/migrations/0055_reminder_mentions.py | 20 +++ .../api/migrations/0055_user_roles_to_array.py | 24 ---- .../api/migrations/0056_allow_blank_user_roles.py | 21 +++ .../0056_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0057_merge_20200716_0751.py | 14 ++ .../apps/api/migrations/0057_reminder_mentions.py | 20 --- .../api/migrations/0058_allow_blank_user_roles.py | 21 --- .../migrations/0058_create_new_filterlist_model.py | 33 +++++ .../migrations/0059_create_new_filterlist_model.py | 33 ----- .../api/migrations/0059_populate_filterlists.py | 153 +++++++++++++++++++++ .../api/migrations/0060_populate_filterlists.py | 153 --------------------- .../migrations/0060_populate_filterlists_fix.py | 85 ++++++++++++ .../migrations/0061_populate_filterlists_fix.py | 85 ------------ .../apps/api/models/bot/off_topic_channel_name.py | 5 - .../apps/api/tests/test_off_topic_channel_names.py | 27 +--- .../api/viewsets/bot/off_topic_channel_name.py | 27 +--- 24 files changed, 431 insertions(+), 472 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py create mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py delete mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py delete mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py new file mode 100644 index 00000000..f18fdfb1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py deleted file mode 100644 index b101d19d..00000000 --- a/pydis_site/apps/api/migrations/0052_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py new file mode 100644 index 00000000..26b3b954 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py deleted file mode 100644 index b51ce1d2..00000000 --- a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_create_news_setting'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py new file mode 100644 index 00000000..7ff3a548 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py deleted file mode 100644 index be9fd948..00000000 --- a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_offtopicchannelname_used'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py new file mode 100644 index 00000000..96230015 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py new file mode 100644 index 00000000..f2a0e638 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py new file mode 100644 index 00000000..d73b450d --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py deleted file mode 100644 index e7b4a983..00000000 --- a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py new file mode 100644 index 00000000..489941c7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_merge_20200714_2027'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py deleted file mode 100644 index ab2696aa..00000000 --- a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py new file mode 100644 index 00000000..47a6d2d4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.14 on 2020-07-16 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_reminder_mentions'), + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py deleted file mode 100644 index fb829a17..00000000 --- a/pydis_site/apps/api/migrations/0057_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0056_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py deleted file mode 100644 index 8f7fddfc..00000000 --- a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0057_reminder_mentions'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py new file mode 100644 index 00000000..aecfdad7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0057_merge_20200716_0751'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py deleted file mode 100644 index eac5542d..00000000 --- a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0058_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py new file mode 100644 index 00000000..8c550191 --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0058_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py deleted file mode 100644 index 35fde95a..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py new file mode 100644 index 00000000..53846f02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py deleted file mode 100644 index eaaafb38..00000000 --- a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0060_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 403c7465..20e77b9f 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -16,11 +16,6 @@ class OffTopicChannelName(ModelReprMixin, models.Model): help_text="The actual channel name that will be used on our Discord server." ) - used = models.BooleanField( - default=False, - help_text="Whether or not this name has already been used during this rotation", - ) - def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 3ab8b22d..bd42cd81 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,14 +10,12 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): - """Return a 401 response when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): - """Return a 401 response when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -26,7 +24,6 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): - """Return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -34,7 +31,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): - """Return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -42,7 +38,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): - """Return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -52,7 +47,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): - """Return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -65,11 +59,10 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_returns_name_in_list(self): - """Return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -83,21 +76,11 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): - """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json(), [self.test_name.name]) - - def test_running_out_of_names_with_random_parameter(self): - """Reset names `used` parameter to `False` when running out of names.""" - url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=2') - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) class CreationTests(APISubdomainTestCase): @@ -110,7 +93,6 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): - """Accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', @@ -122,7 +104,6 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): - """Return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -131,7 +112,6 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): - """Return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -154,21 +134,18 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): - """Return 404 reponse when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): - """Return 204 response when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): - """Name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 826ad25e..d6da2399 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,4 +1,3 @@ -from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -21,9 +20,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): Return all known off-topic channel names from the database. If the `random_items` query parameter is given, for example using... $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 - ... then the API will return `5` random items from the database - that is not used in current rotation. - When running out of names, API will mark all names to not used and start new rotation. + ... then the API will return `5` random items from the database. #### Response format Return a list of off-topic-channel names: @@ -109,27 +106,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('used', '?')[:random_count] - - # When any name is used in our listing then this means we reached end of round - # and we need to reset all other names `used` to False - if any(offtopic_name.used for offtopic_name in queryset): - # These names that we just got have to be excluded from updating used to False - self.get_queryset().update( - used=Case( - When( - name__in=(offtopic_name.name for offtopic_name in queryset), - then=Value(True) - ), - default=Value(False) - ) - ) - else: - # Otherwise mark selected names `used` to True - self.get_queryset().filter( - name__in=(offtopic_name.name for offtopic_name in queryset) - ).update(used=True) - + queryset = self.get_queryset().order_by('?')[:random_count] serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From c1ec2f1f9fe443cdde3750b1df1c3d969df3e67e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:20:25 +0300 Subject: Revert "Revert pull request #348" This reverts commit dae68c46831ef14e6a0715add1025f9af1b5a73d. --- .../api/migrations/0051_create_news_setting.py | 25 ---- .../api/migrations/0052_create_news_setting.py | 25 ++++ .../api/migrations/0052_remove_user_avatar_hash.py | 17 --- .../migrations/0053_offtopicchannelname_used.py | 18 +++ .../api/migrations/0053_user_roles_to_array.py | 24 ---- .../api/migrations/0054_remove_user_avatar_hash.py | 17 +++ .../0054_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0055_merge_20200714_2027.py | 14 -- .../apps/api/migrations/0055_reminder_mentions.py | 20 --- .../api/migrations/0055_user_roles_to_array.py | 24 ++++ .../api/migrations/0056_allow_blank_user_roles.py | 21 --- .../0056_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0057_merge_20200716_0751.py | 14 -- .../apps/api/migrations/0057_reminder_mentions.py | 20 +++ .../api/migrations/0058_allow_blank_user_roles.py | 21 +++ .../migrations/0058_create_new_filterlist_model.py | 33 ----- .../migrations/0059_create_new_filterlist_model.py | 33 +++++ .../api/migrations/0059_populate_filterlists.py | 153 --------------------- .../api/migrations/0060_populate_filterlists.py | 153 +++++++++++++++++++++ .../migrations/0060_populate_filterlists_fix.py | 85 ------------ .../migrations/0061_populate_filterlists_fix.py | 85 ++++++++++++ .../apps/api/models/bot/off_topic_channel_name.py | 5 + .../apps/api/tests/test_off_topic_channel_names.py | 27 +++- .../api/viewsets/bot/off_topic_channel_name.py | 27 +++- 24 files changed, 472 insertions(+), 431 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py delete mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py delete mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py create mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py create mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py deleted file mode 100644 index f18fdfb1..00000000 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py new file mode 100644 index 00000000..b101d19d --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py deleted file mode 100644 index 26b3b954..00000000 --- a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py new file mode 100644 index 00000000..b51ce1d2 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py deleted file mode 100644 index 7ff3a548..00000000 --- a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py new file mode 100644 index 00000000..be9fd948 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_offtopicchannelname_used'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py deleted file mode 100644 index 96230015..00000000 --- a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py deleted file mode 100644 index f2a0e638..00000000 --- a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py deleted file mode 100644 index d73b450d..00000000 --- a/pydis_site/apps/api/migrations/0055_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py new file mode 100644 index 00000000..e7b4a983 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py deleted file mode 100644 index 489941c7..00000000 --- a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_merge_20200714_2027'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py new file mode 100644 index 00000000..ab2696aa --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py deleted file mode 100644 index 47a6d2d4..00000000 --- a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-16 07:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_reminder_mentions'), - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py new file mode 100644 index 00000000..fb829a17 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py new file mode 100644 index 00000000..8f7fddfc --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0057_reminder_mentions'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py deleted file mode 100644 index aecfdad7..00000000 --- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0057_merge_20200716_0751'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py new file mode 100644 index 00000000..eac5542d --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0058_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py deleted file mode 100644 index 8c550191..00000000 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0058_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py new file mode 100644 index 00000000..35fde95a --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py deleted file mode 100644 index 53846f02..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py new file mode 100644 index 00000000..eaaafb38 --- /dev/null +++ b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0060_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 20e77b9f..403c7465 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -16,6 +16,11 @@ class OffTopicChannelName(ModelReprMixin, models.Model): help_text="The actual channel name that will be used on our Discord server." ) + used = models.BooleanField( + default=False, + help_text="Whether or not this name has already been used during this rotation", + ) + def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index bd42cd81..3ab8b22d 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,12 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): + """Return a 401 response when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): + """Return a 401 response when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -24,6 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): + """Return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -31,6 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): + """Return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): + """Return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -47,6 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): + """Return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -59,10 +65,11 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): + """Return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -76,11 +83,21 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): + """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json(), [self.test_name.name]) + + def test_running_out_of_names_with_random_parameter(self): + """Reset names `used` parameter to `False` when running out of names.""" + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=2') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) class CreationTests(APISubdomainTestCase): @@ -93,6 +110,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): + """Accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', @@ -104,6 +122,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): + """Return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -112,6 +131,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): + """Return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -134,18 +154,21 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): + """Return 404 reponse when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): + """Return 204 response when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): + """Name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index d6da2399..826ad25e 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,3 +1,4 @@ +from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -20,7 +21,9 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): Return all known off-topic channel names from the database. If the `random_items` query parameter is given, for example using... $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 - ... then the API will return `5` random items from the database. + ... then the API will return `5` random items from the database + that is not used in current rotation. + When running out of names, API will mark all names to not used and start new rotation. #### Response format Return a list of off-topic-channel names: @@ -106,7 +109,27 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('?')[:random_count] + queryset = self.get_queryset().order_by('used', '?')[:random_count] + + # When any name is used in our listing then this means we reached end of round + # and we need to reset all other names `used` to False + if any(offtopic_name.used for offtopic_name in queryset): + # These names that we just got have to be excluded from updating used to False + self.get_queryset().update( + used=Case( + When( + name__in=(offtopic_name.name for offtopic_name in queryset), + then=Value(True) + ), + default=Value(False) + ) + ) + else: + # Otherwise mark selected names `used` to True + self.get_queryset().filter( + name__in=(offtopic_name.name for offtopic_name in queryset) + ).update(used=True) + serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From a95603405a1f7d80845fdf8e4719ce7f4c385594 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Mon, 5 Oct 2020 18:19:45 +0530 Subject: return next and previous page numbers in paginator instead of links --- pydis_site/apps/api/viewsets/bot/user.py | 41 +++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index d015fe71..0dd529be 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from rest_framework import status from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination @@ -16,6 +18,30 @@ class UserListPagination(PageNumberPagination): page_size = 10000 page_size_query_param = "page_size" + def get_next_page_number(self) -> int: + """Get the next page number.""" + if not self.page.has_next(): + return None + page_number = self.page.next_page_number() + return page_number + + def get_previous_page_number(self) -> int: + """Get the previous page number.""" + if not self.page.has_previous(): + return None + + page_number = self.page.previous_page_number() + return page_number + + def get_paginated_response(self, data: list) -> Response: + """Override method to send modified response.""" + return Response(OrderedDict([ + ('count', self.page.paginator.count), + ('next_page_no', self.get_next_page_number()), + ('previous_page_no', self.get_previous_page_number()), + ('results', data) + ])) + class UserViewSet(ModelViewSet): """ @@ -193,13 +219,6 @@ class UserViewSet(ModelViewSet): filtered_instances = queryset.filter(id__in=object_ids) - if filtered_instances.count() != len(object_ids): - # If all user objects passed in request.body are not present in the database. - resp = { - "Error": "User object not found." - } - return Response(resp, status=status.HTTP_404_NOT_FOUND) - serializer = self.get_serializer( instance=filtered_instances, data=request.data, @@ -207,7 +226,7 @@ class UserViewSet(ModelViewSet): partial=True ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data, status=status.HTTP_200_OK) -- cgit v1.2.3 From 3eee5e76894995cea87391d60c9749eb66b3187a Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Tue, 6 Oct 2020 21:21:53 +0530 Subject: Update docstring of UserViewSet and correct function annotation on UserListPagination --- pydis_site/apps/api/viewsets/bot/user.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 0dd529be..ebea6a45 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,3 +1,4 @@ +import typing from collections import OrderedDict from rest_framework import status @@ -18,14 +19,14 @@ class UserListPagination(PageNumberPagination): page_size = 10000 page_size_query_param = "page_size" - def get_next_page_number(self) -> int: + def get_next_page_number(self) -> typing.Optional[int]: """Get the next page number.""" if not self.page.has_next(): return None page_number = self.page.next_page_number() return page_number - def get_previous_page_number(self) -> int: + def get_previous_page_number(self) -> typing.Optional[int]: """Get the previous page number.""" if not self.page.has_previous(): return None @@ -54,8 +55,8 @@ class UserViewSet(ModelViewSet): #### Response format >>> { ... 'count': 95000, - ... 'next': "http://api.pythondiscord.com/bot/users?page=2", - ... 'previous': None, + ... 'next_page_no': "2", + ... 'previous_page_no': None, ... 'results': [ ... { ... 'id': 409107086526644234, -- cgit v1.2.3 From c8c6cb8754bd0917e35eb157925028a9b6f1dcb9 Mon Sep 17 00:00:00 2001 From: Lucas Lindström Date: Tue, 6 Oct 2020 21:29:34 +0200 Subject: Added metricity db connection and user bot API --- docker-compose.yml | 3 +++ postgres/init.sql | 34 +++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/user.py | 46 ++++++++++++++++++++++++++++++++ pydis_site/settings.py | 3 ++- 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 postgres/init.sql (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/docker-compose.yml b/docker-compose.yml index 73d2ff85..7287d8d5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: POSTGRES_DB: pysite POSTGRES_PASSWORD: pysite POSTGRES_USER: pysite + volumes: + - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql web: build: @@ -40,6 +42,7 @@ services: - staticfiles:/var/www/static environment: DATABASE_URL: postgres://pysite:pysite@postgres:5432/pysite + METRICITY_DB_URL: postgres://pysite:pysite@postgres:5432/metricity SECRET_KEY: suitable-for-development-only STATIC_ROOT: /var/www/static diff --git a/postgres/init.sql b/postgres/init.sql new file mode 100644 index 00000000..fd29ddbc --- /dev/null +++ b/postgres/init.sql @@ -0,0 +1,34 @@ +CREATE DATABASE metricity; + +\c metricity; + +CREATE TABLE users ( + id varchar(255), + name varchar(255) not null, + avatar_hash varchar(255), + joined_at timestamp not null, + created_at timestamp not null, + is_staff boolean not null, + opt_out boolean default false, + bot boolean default false, + is_guild boolean default true, + is_verified boolean default false, + public_flags text default '{}', + verified_at timestamp, + primary key(id) +); + +INSERT INTO users VALUES ( + 0, + 'foo', + 'bar', + current_timestamp, + current_timestamp, + false, + false, + false, + true, + false, + '{}', + NULL +); diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 9571b3d7..0eeacbb3 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,3 +1,10 @@ +import json + +from django.db import connections +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin @@ -53,6 +60,29 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): - 200: returned on success - 404: if a user with the given `snowflake` could not be found + ### GET /bot/users//metricity_data + Gets metricity data for a single user by ID. + + #### Response format + >>> { + ... "id": "0", + ... "name": "foo", + ... "avatar_hash": "bar", + ... "joined_at": "2020-10-06T18:17:30.101677", + ... "created_at": "2020-10-06T18:17:30.101677", + ... "is_staff": False, + ... "opt_out": False, + ... "bot": False, + ... "is_guild": True, + ... "is_verified": False, + ... "public_flags": {}, + ... "verified_at": null + ...} + + #### Status codes + - 200: returned on success + - 404: if a user with the given `snowflake` could not be found + ### POST /bot/users Adds a single or multiple new users. The roles attached to the user(s) must be roles known by the site. @@ -115,7 +145,23 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Status codes - 204: returned on success - 404: if a user with the given `snowflake` does not exist + + """ serializer_class = UserSerializer queryset = User.objects + + @action(detail=True) + def metricity_data(self, request: Request, pk: str = None) -> Response: + """Request handler for metricity_data endpoint.""" + user = self.get_object() + column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff", + "opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"] + with connections['metricity'].cursor() as cursor: + query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'" + cursor.execute(query, [user.id]) + values = cursor.fetchone() + data = dict(zip(column_keys, values)) + data["public_flags"] = json.loads(data["public_flags"]) + return Response(data, status=status.HTTP_200_OK) diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 3769fa25..2e78e458 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -172,7 +172,8 @@ WSGI_APPLICATION = 'pydis_site.wsgi.application' # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { - 'default': env.db() + 'default': env.db(), + 'metricity': env.db('METRICITY_DB_URL'), } # Password validation -- cgit v1.2.3 From a188ab8ebfa9299addb7fd0effce2de7a0509645 Mon Sep 17 00:00:00 2001 From: Lucas Lindström Date: Tue, 6 Oct 2020 22:02:08 +0200 Subject: Fix minor style issue. --- pydis_site/apps/api/viewsets/bot/user.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 0eeacbb3..059bc0f0 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -145,8 +145,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Status codes - 204: returned on success - 404: if a user with the given `snowflake` does not exist - - """ serializer_class = UserSerializer -- cgit v1.2.3 From 15086b4ab392a8bdc6c33414f0c4e2a294f4a2ef Mon Sep 17 00:00:00 2001 From: Lucas Lindström Date: Tue, 6 Oct 2020 22:40:14 +0200 Subject: Added total message count to metricity data response. --- postgres/init.sql | 16 ++++++++++++++++ pydis_site/apps/api/viewsets/bot/user.py | 9 ++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/postgres/init.sql b/postgres/init.sql index fd29ddbc..45ad440c 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -32,3 +32,19 @@ INSERT INTO users VALUES ( '{}', NULL ); + +CREATE TABLE messages ( + id varchar(255), + author_id varchar(255) references users(id), + primary key(id) +); + +INSERT INTO messages VALUES( + 0, + 0 +); + +INSERT INTO messages VALUES( + 1, + 0 +); diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 059bc0f0..b3d880cc 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -76,7 +76,8 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): ... "is_guild": True, ... "is_verified": False, ... "public_flags": {}, - ... "verified_at": null + ... "verified_at": None, + ... "total_messages": 2 ...} #### Status codes @@ -157,9 +158,15 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff", "opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"] with connections['metricity'].cursor() as cursor: + # Get user data query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'" cursor.execute(query, [user.id]) values = cursor.fetchone() data = dict(zip(column_keys, values)) data["public_flags"] = json.loads(data["public_flags"]) + + # Get message count + cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id]) + data["total_messages"], = cursor.fetchone() + return Response(data, status=status.HTTP_200_OK) -- cgit v1.2.3 From e83f445a9b8d2db4523e261759bb73ea83ed54c3 Mon Sep 17 00:00:00 2001 From: Lucas Lindström Date: Tue, 6 Oct 2020 23:56:47 +0200 Subject: Reduce metricity db setup script and API response to the bare necessities. --- postgres/init.sql | 28 ++++------------------------ pydis_site/apps/api/viewsets/bot/user.py | 29 ++++------------------------- 2 files changed, 8 insertions(+), 49 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/postgres/init.sql b/postgres/init.sql index 45ad440c..922ce1ad 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -3,39 +3,19 @@ CREATE DATABASE metricity; \c metricity; CREATE TABLE users ( - id varchar(255), - name varchar(255) not null, - avatar_hash varchar(255), - joined_at timestamp not null, - created_at timestamp not null, - is_staff boolean not null, - opt_out boolean default false, - bot boolean default false, - is_guild boolean default true, - is_verified boolean default false, - public_flags text default '{}', + id varchar, verified_at timestamp, primary key(id) ); INSERT INTO users VALUES ( 0, - 'foo', - 'bar', - current_timestamp, - current_timestamp, - false, - false, - false, - true, - false, - '{}', - NULL + current_timestamp ); CREATE TABLE messages ( - id varchar(255), - author_id varchar(255) references users(id), + id varchar, + author_id varchar references users(id), primary key(id) ); diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index b3d880cc..1b1af841 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,5 +1,3 @@ -import json - from django.db import connections from rest_framework import status from rest_framework.decorators import action @@ -65,18 +63,7 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Response format >>> { - ... "id": "0", - ... "name": "foo", - ... "avatar_hash": "bar", - ... "joined_at": "2020-10-06T18:17:30.101677", - ... "created_at": "2020-10-06T18:17:30.101677", - ... "is_staff": False, - ... "opt_out": False, - ... "bot": False, - ... "is_guild": True, - ... "is_verified": False, - ... "public_flags": {}, - ... "verified_at": None, + ... "verified_at": "2020-10-06T21:54:23.540766", ... "total_messages": 2 ...} @@ -155,18 +142,10 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): def metricity_data(self, request: Request, pk: str = None) -> Response: """Request handler for metricity_data endpoint.""" user = self.get_object() - column_keys = ["id", "name", "avatar_hash", "joined_at", "created_at", "is_staff", - "opt_out", "bot", "is_guild", "is_verified", "public_flags", "verified_at"] with connections['metricity'].cursor() as cursor: - # Get user data - query = f"SELECT {','.join(column_keys)} FROM users WHERE id = '%s'" - cursor.execute(query, [user.id]) - values = cursor.fetchone() - data = dict(zip(column_keys, values)) - data["public_flags"] = json.loads(data["public_flags"]) - - # Get message count + data = {} + cursor.execute("SELECT verified_at FROM users WHERE id = '%s'", [user.id]) + data["verified_at"], = cursor.fetchone() cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id]) data["total_messages"], = cursor.fetchone() - return Response(data, status=status.HTTP_200_OK) -- cgit v1.2.3 From 5d3d305648acce800769c620760b3a642b399276 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 7 Oct 2020 09:42:28 +0530 Subject: Document changes made to UserListSerializer in UserViewSet --- pydis_site/apps/api/viewsets/bot/user.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index ebea6a45..54723890 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -104,6 +104,8 @@ class UserViewSet(ModelViewSet): ### POST /bot/users Adds a single or multiple new users. The roles attached to the user(s) must be roles known by the site. + User creation process will be skipped if user is already present in the database. + multiple users with the same id in the request data will raise an error. #### Request body >>> { @@ -120,6 +122,7 @@ class UserViewSet(ModelViewSet): #### Status codes - 201: returned on success - 400: if one of the given roles does not exist, or one of the given fields is invalid + - 400: if multiple user objects with the same id is given. ### PUT /bot/users/ Update the user with the given `snowflake`. -- cgit v1.2.3 From 5388034d9a00e41f18ba5f476a2c3e347d4bd569 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Wed, 7 Oct 2020 11:24:31 +0530 Subject: update documentation --- pydis_site/apps/api/viewsets/bot/user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 54723890..740fc439 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -105,7 +105,6 @@ class UserViewSet(ModelViewSet): Adds a single or multiple new users. The roles attached to the user(s) must be roles known by the site. User creation process will be skipped if user is already present in the database. - multiple users with the same id in the request data will raise an error. #### Request body >>> { @@ -122,7 +121,7 @@ class UserViewSet(ModelViewSet): #### Status codes - 201: returned on success - 400: if one of the given roles does not exist, or one of the given fields is invalid - - 400: if multiple user objects with the same id is given. + - 400: if multiple user objects with the same id are given. ### PUT /bot/users/ Update the user with the given `snowflake`. @@ -208,8 +207,9 @@ class UserViewSet(ModelViewSet): ... ] #### Status codes - - 200: Returned on success. + - 200: returned on success. - 400: if the request body was invalid, see response body for details. + - 404: if the user with the given id does not exist. """ queryset = self.get_queryset() try: -- cgit v1.2.3 From 484eba7715fcbcc195d66f5a60ff56c8167ecf0e Mon Sep 17 00:00:00 2001 From: Lucas Lindström Date: Thu, 8 Oct 2020 00:17:47 +0200 Subject: Broke out metricity connection into an abstraction and added metricity endpoint unit tests. --- .coveragerc | 1 + pydis_site/apps/api/models/bot/metricity.py | 42 +++++++++++++++++++++ pydis_site/apps/api/tests/test_users.py | 58 +++++++++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/user.py | 17 +++++---- 4 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 pydis_site/apps/api/models/bot/metricity.py (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/.coveragerc b/.coveragerc index f5ddf08d..0cccc47c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,6 +12,7 @@ omit = */admin.py */apps.py */urls.py + pydis_site/apps/api/models/bot/metricity.py pydis_site/wsgi.py pydis_site/settings.py pydis_site/utils/resources.py diff --git a/pydis_site/apps/api/models/bot/metricity.py b/pydis_site/apps/api/models/bot/metricity.py new file mode 100644 index 00000000..25b42fa2 --- /dev/null +++ b/pydis_site/apps/api/models/bot/metricity.py @@ -0,0 +1,42 @@ +from django.db import connections + + +class NotFound(Exception): + """Raised when an entity cannot be found.""" + + pass + + +class Metricity: + """Abstraction for a connection to the metricity database.""" + + def __init__(self): + self.cursor = connections['metricity'].cursor() + + def __enter__(self): + return self + + def __exit__(self, *_): + self.cursor.close() + + def user(self, user_id: str) -> dict: + """Query a user's data.""" + columns = ["verified_at"] + query = f"SELECT {','.join(columns)} FROM users WHERE id = '%s'" + self.cursor.execute(query, [user_id]) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return dict(zip(columns, values)) + + def total_messages(self, user_id: str) -> int: + """Query total number of messages for a user.""" + self.cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user_id]) + values = self.cursor.fetchone() + + if not values: + raise NotFound() + + return values[0] diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index a02fce8a..76a21d3a 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -1,7 +1,10 @@ +from unittest.mock import patch + from django_hosts.resolvers import reverse from .base import APISubdomainTestCase from ..models import Role, User +from ..models.bot.metricity import NotFound class UnauthedUserAPITests(APISubdomainTestCase): @@ -170,3 +173,58 @@ class UserModelTests(APISubdomainTestCase): def test_correct_username_formatting(self): """Tests the username property with both name and discriminator formatted together.""" self.assertEqual(self.user_with_roles.username, "Test User with two roles#0001") + + +class UserMetricityTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + User.objects.create( + id=0, + name="Test user", + discriminator=1, + in_guild=True, + ) + + def test_get_metricity_data(self): + # Given + verified_at = "foo" + total_messages = 1 + self.mock_metricity_user(verified_at, total_messages) + + # When + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + "verified_at": verified_at, + "total_messages": total_messages, + }) + + def test_no_metricity_user(self): + # Given + self.mock_no_metricity_user() + + # When + url = reverse('bot:user-metricity-data', args=[0], host='api') + response = self.client.get(url) + + # Then + self.assertEqual(response.status_code, 404) + + def mock_metricity_user(self, verified_at, total_messages): + patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") + self.metricity = patcher.start() + self.addCleanup(patcher.stop) + self.metricity = self.metricity.return_value.__enter__.return_value + self.metricity.user.return_value = dict(verified_at=verified_at) + self.metricity.total_messages.return_value = total_messages + + def mock_no_metricity_user(self): + patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") + self.metricity = patcher.start() + self.addCleanup(patcher.stop) + self.metricity = self.metricity.return_value.__enter__.return_value + self.metricity.user.side_effect = NotFound() + self.metricity.total_messages.side_effect = NotFound() diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 1b1af841..352d77c0 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,4 +1,3 @@ -from django.db import connections from rest_framework import status from rest_framework.decorators import action from rest_framework.request import Request @@ -6,6 +5,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework_bulk import BulkCreateModelMixin +from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer @@ -142,10 +142,11 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): def metricity_data(self, request: Request, pk: str = None) -> Response: """Request handler for metricity_data endpoint.""" user = self.get_object() - with connections['metricity'].cursor() as cursor: - data = {} - cursor.execute("SELECT verified_at FROM users WHERE id = '%s'", [user.id]) - data["verified_at"], = cursor.fetchone() - cursor.execute("SELECT COUNT(*) FROM messages WHERE author_id = '%s'", [user.id]) - data["total_messages"], = cursor.fetchone() - return Response(data, status=status.HTTP_200_OK) + with Metricity() as metricity: + try: + data = metricity.user(user.id) + data["total_messages"] = metricity.total_messages(user.id) + return Response(data, status=status.HTTP_200_OK) + except NotFound: + return Response(dict(detail="User not found in metricity"), + status=status.HTTP_404_NOT_FOUND) -- cgit v1.2.3 From cb32322b8bacabecccdf896d0f7db0355177ac72 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 13:50:50 +0530 Subject: raise ValidationError if users have same ID in request data during bulk patch --- pydis_site/apps/api/viewsets/bot/user.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 740fc439..46e682d8 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -6,7 +6,7 @@ from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import ModelSerializer +from rest_framework.serializers import ModelSerializer, ValidationError from rest_framework.viewsets import ModelViewSet from pydis_site.apps.api.models.bot.user import User @@ -212,14 +212,20 @@ class UserViewSet(ModelViewSet): - 404: if the user with the given id does not exist. """ queryset = self.get_queryset() - try: - object_ids = [item["id"] for item in request.data] - except KeyError: - # user ID not provided in request body. - resp = { - "Error": "User ID not provided." - } - return Response(resp, status=status.HTTP_400_BAD_REQUEST) + object_ids = set() + for data in request.data: + try: + if data["id"] in object_ids: + # If request data contains users with same ID. + raise ValidationError( + {"id": [f"User with ID {data['id']} given multiple times."]} + ) + except KeyError: + # If user ID not provided in request body. + raise ValidationError( + {"id": ["This field is required."]} + ) + object_ids.add(data["id"]) filtered_instances = queryset.filter(id__in=object_ids) -- cgit v1.2.3 From ffab239d674ada91fc4d6c265f6b1aa1c82b9c2c Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 14:12:19 +0530 Subject: update documentation --- pydis_site/apps/api/viewsets/bot/user.py | 70 ++++++++++++++++---------------- 1 file changed, 34 insertions(+), 36 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 46e682d8..77142c30 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -73,9 +73,9 @@ class UserViewSet(ModelViewSet): ... ] ... } - #### Query Parameters - - page_size: Number of Users in one page. - - page: Page number + #### Optional Query Parameters + - page_size: number of Users in one page, defaults to 10,000 + - page: page number #### Status codes - 200: returned on success @@ -104,7 +104,7 @@ class UserViewSet(ModelViewSet): ### POST /bot/users Adds a single or multiple new users. The roles attached to the user(s) must be roles known by the site. - User creation process will be skipped if user is already present in the database. + Users that already exist in the database will be skipped. #### Request body >>> { @@ -121,7 +121,7 @@ class UserViewSet(ModelViewSet): #### Status codes - 201: returned on success - 400: if one of the given roles does not exist, or one of the given fields is invalid - - 400: if multiple user objects with the same id are given. + - 400: if multiple user objects with the same id are given ### PUT /bot/users/ Update the user with the given `snowflake`. @@ -159,6 +159,34 @@ class UserViewSet(ModelViewSet): - 400: if the request body was invalid, see response body for details - 404: if the user with the given `snowflake` could not be found + ### BULK PATCH /bot/users/bulk_patch + Update users with the given `ids` and `details`. + `id` field and at least one other field is mandatory. + + #### Request body + >>> [ + ... { + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool + ... }, + ... { + ... 'id': int, + ... 'name': str, + ... 'discriminator': 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 + - 400: if multiple user objects with the same id are given + - 404: if the user with the given id does not exist + ### DELETE /bot/users/ Deletes the user with the given `snowflake`. @@ -180,37 +208,7 @@ class UserViewSet(ModelViewSet): @action(detail=False, methods=["PATCH"], name='user-bulk-patch') def bulk_patch(self, request: Request) -> Response: - """ - Update multiple User objects in a single request. - - ## Route - ### PATCH /bot/users/bulk_patch - Update all users with the IDs. - `id` field is mandatory, rest are optional. - - #### Request body - >>> [ - ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool - ... }, - ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': 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 user with the given id does not exist. - """ + """Update multiple User objects in a single request.""" queryset = self.get_queryset() object_ids = set() for data in request.data: -- cgit v1.2.3 From e7413978556567d86c92ff643b6d2173d09f44b7 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 14:32:28 +0530 Subject: correct indentation --- pydis_site/apps/api/viewsets/bot/user.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 77142c30..22e61915 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -166,18 +166,18 @@ class UserViewSet(ModelViewSet): #### Request body >>> [ ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool ... }, ... { - ... 'id': int, - ... 'name': str, - ... 'discriminator': int, - ... 'roles': List[int], - ... 'in_guild': bool + ... 'id': int, + ... 'name': str, + ... 'discriminator': int, + ... 'roles': List[int], + ... 'in_guild': bool ... }, ... ] -- cgit v1.2.3 From 570f0cf226ebabbd4f41a19ded90f70d0ec96f9a Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 22:49:57 +0530 Subject: return empty list as response for bulk creation of users --- pydis_site/apps/api/serializers.py | 4 ++-- pydis_site/apps/api/tests/test_users.py | 2 +- pydis_site/apps/api/viewsets/bot/user.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index efd4f7ce..31293c86 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -260,8 +260,8 @@ class UserListSerializer(ListSerializer): seen.add(user_dict["id"]) new_users.append(User(**user_dict)) - users = User.objects.bulk_create(new_users, ignore_conflicts=True) - return User.objects.filter(id__in=[user.id for user in users]) + User.objects.bulk_create(new_users, ignore_conflicts=True) + return [] def update(self, instance: QuerySet, validated_data: list) -> list: """ diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 8ed56e83..825e4edb 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -96,7 +96,7 @@ class CreationTests(APISubdomainTestCase): response = self.client.post(url, data=data) self.assertEqual(response.status_code, 201) - self.assertEqual(response.json(), data) + self.assertEqual(response.json(), []) def test_returns_400_for_unknown_role_id(self): url = reverse('bot:user-list', host='api') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 22e61915..5c509e50 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -116,7 +116,8 @@ class UserViewSet(ModelViewSet): ... } Alternatively, request users can be POSTed as a list of above objects, - in which case multiple users will be created at once. + in which case multiple users will be created at once. In this case, + the response is an empty list. #### Status codes - 201: returned on success -- cgit v1.2.3 From e405b60de8f46ad1ef0ec8b2f0ea8cdd12ef2e55 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Thu, 8 Oct 2020 23:02:42 +0530 Subject: supress warning: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: QuerySet. --- pydis_site/apps/api/viewsets/bot/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 5c509e50..9ddd13d4 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -197,7 +197,7 @@ class UserViewSet(ModelViewSet): """ serializer_class = UserSerializer - queryset = User.objects.all() + queryset = User.objects.all().order_by("id") pagination_class = UserListPagination def get_serializer(self, *args, **kwargs) -> ModelSerializer: -- cgit v1.2.3 From 1bef883886f3335a441b496b35df9e25b3ba0e91 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Fri, 9 Oct 2020 00:02:24 +0530 Subject: Move Validation checks to serializer from viewset --- pydis_site/apps/api/serializers.py | 23 +++++++++++++++++++++-- pydis_site/apps/api/viewsets/bot/user.py | 22 ++-------------------- 2 files changed, 23 insertions(+), 22 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index ee950cf4..25c5c82e 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -263,13 +263,32 @@ class UserListSerializer(ListSerializer): User.objects.bulk_create(new_users, ignore_conflicts=True) return [] - def update(self, instance: QuerySet, validated_data: list) -> list: + def update(self, queryset: QuerySet, validated_data: list) -> list: """ Override update method to support bulk updates. ref:https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update """ - instance_mapping = {user.id: user for user in instance} + object_ids = set() + + for data in validated_data: + try: + if data["id"] in object_ids: + # If request data contains users with same ID. + raise ValidationError( + {"id": [f"User with ID {data['id']} given multiple times."]} + ) + except KeyError: + # If user ID not provided in request body. + raise ValidationError( + {"id": ["This field is required."]} + ) + object_ids.add(data["id"]) + + # filter queryset + filtered_instances = queryset.filter(id__in=object_ids) + + instance_mapping = {user.id: user for user in filtered_instances} updated = [] fields_to_update = set() diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 9ddd13d4..3e4b627e 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -6,7 +6,7 @@ from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import ModelSerializer, ValidationError +from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet from pydis_site.apps.api.models.bot.user import User @@ -210,26 +210,8 @@ class UserViewSet(ModelViewSet): @action(detail=False, methods=["PATCH"], name='user-bulk-patch') def bulk_patch(self, request: Request) -> Response: """Update multiple User objects in a single request.""" - queryset = self.get_queryset() - object_ids = set() - for data in request.data: - try: - if data["id"] in object_ids: - # If request data contains users with same ID. - raise ValidationError( - {"id": [f"User with ID {data['id']} given multiple times."]} - ) - except KeyError: - # If user ID not provided in request body. - raise ValidationError( - {"id": ["This field is required."]} - ) - object_ids.add(data["id"]) - - filtered_instances = queryset.filter(id__in=object_ids) - serializer = self.get_serializer( - instance=filtered_instances, + instance=self.get_queryset(), data=request.data, many=True, partial=True -- cgit v1.2.3 From 990d8cfc98ec71a31ea3ef659cb109547f987e31 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 19:56:54 +0300 Subject: Include voice ban status on metricity data --- pydis_site/apps/api/viewsets/bot/user.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 3ab71186..19f55555 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,6 +1,7 @@ import typing from collections import OrderedDict +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from rest_framework import status from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination @@ -9,6 +10,7 @@ from rest_framework.response import Response from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet +from pydis_site.apps.api.models.bot.infraction import Infraction from pydis_site.apps.api.models.bot.metricity import Metricity, NotFound from pydis_site.apps.api.models.bot.user import User from pydis_site.apps.api.serializers import UserSerializer @@ -240,10 +242,21 @@ class UserViewSet(ModelViewSet): def metricity_data(self, request: Request, pk: str = None) -> Response: """Request handler for metricity_data endpoint.""" user = self.get_object() + + try: + Infraction.objects.get(user__id=user.id, active=True, type="voice_ban") + except ObjectDoesNotExist: + voice_banned = False + except MultipleObjectReturned: + voice_banned = True + else: + voice_banned = True + with Metricity() as metricity: try: data = metricity.user(user.id) data["total_messages"] = metricity.total_messages(user.id) + data["voice_banned"] = voice_banned return Response(data, status=status.HTTP_200_OK) except NotFound: return Response(dict(detail="User not found in metricity"), -- cgit v1.2.3 From d80c7f18fadd1abc1aa2c080c2de6e7dee395883 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:19:53 +0300 Subject: Fix wrong exception name --- pydis_site/apps/api/viewsets/bot/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 19f55555..367f6b65 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -247,7 +247,7 @@ class UserViewSet(ModelViewSet): Infraction.objects.get(user__id=user.id, active=True, type="voice_ban") except ObjectDoesNotExist: voice_banned = False - except MultipleObjectReturned: + except MultipleObjectsReturned: voice_banned = True else: voice_banned = True -- cgit v1.2.3 From 9c4ed0b98e4f34fe3a589e4e26f2b31e6c5169f9 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 20:35:32 +0300 Subject: Don't catch multiple object returned exception --- pydis_site/apps/api/viewsets/bot/user.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 367f6b65..5205dc97 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -1,7 +1,7 @@ import typing from collections import OrderedDict -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist from rest_framework import status from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination @@ -247,8 +247,6 @@ class UserViewSet(ModelViewSet): Infraction.objects.get(user__id=user.id, active=True, type="voice_ban") except ObjectDoesNotExist: voice_banned = False - except MultipleObjectsReturned: - voice_banned = True else: voice_banned = True -- cgit v1.2.3 From b63d79ac0f663d2062082d8cccf7dc0e072ceeb1 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 27 Oct 2020 01:51:55 +0000 Subject: Update viewset to include activity_blocks response item --- pydis_site/apps/api/viewsets/bot/user.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 5205dc97..79f90163 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -110,7 +110,9 @@ class UserViewSet(ModelViewSet): #### Response format >>> { ... "verified_at": "2020-10-06T21:54:23.540766", - ... "total_messages": 2 + ... "total_messages": 2, + ... "voice_banned": False, + ... "activity_blocks": 1 ...} #### Status codes @@ -255,6 +257,7 @@ class UserViewSet(ModelViewSet): data = metricity.user(user.id) data["total_messages"] = metricity.total_messages(user.id) data["voice_banned"] = voice_banned + data["activity_blocks"] = metricity.total_message_blocks(user.id) return Response(data, status=status.HTTP_200_OK) except NotFound: return Response(dict(detail="User not found in metricity"), -- cgit v1.2.3 From 9efd0cb678e7d55db15272c17b996188fda4a211 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 5 Dec 2020 19:19:10 +0200 Subject: Include DestroyModelMixin to infractions view for DELETE method Added this mixin and documented this in doctoring. --- pydis_site/apps/api/viewsets/bot/infraction.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py index edec0a1e..423e806e 100644 --- a/pydis_site/apps/api/viewsets/bot/infraction.py +++ b/pydis_site/apps/api/viewsets/bot/infraction.py @@ -5,6 +5,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.mixins import ( CreateModelMixin, + DestroyModelMixin, ListModelMixin, RetrieveModelMixin ) @@ -18,7 +19,13 @@ from pydis_site.apps.api.serializers import ( ) -class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): +class InfractionViewSet( + CreateModelMixin, + RetrieveModelMixin, + ListModelMixin, + GenericViewSet, + DestroyModelMixin +): """ View providing CRUD operations on infractions for Discord users. @@ -108,6 +115,13 @@ class InfractionViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge - 400: if a field in the request body is invalid or disallowed - 404: if an infraction with the given `id` could not be found + ### DELETE /bot/infractions/ + Delete the infraction with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a infraction with the given `id` does not exist + ### Expanded routes All routes support expansion of `user` and `actor` in responses. To use an expanded route, append `/expanded` to the end of the route e.g. `GET /bot/infractions/expanded`. -- cgit v1.2.3 From eff717bb38e5b556751ba4f23a53cc6662baa7dd Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 19 Dec 2020 03:43:23 +0000 Subject: Update verified_at fields to joined_at fields --- postgres/init.sql | 2 +- pydis_site/apps/api/tests/test_users.py | 10 +++++----- pydis_site/apps/api/viewsets/bot/user.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/postgres/init.sql b/postgres/init.sql index c77fec9e..740063e7 100644 --- a/postgres/init.sql +++ b/postgres/init.sql @@ -4,7 +4,7 @@ CREATE DATABASE metricity; CREATE TABLE users ( id varchar, - verified_at timestamp, + joined_at timestamp, primary key(id) ); diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index c422f895..69bbfefc 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -407,10 +407,10 @@ class UserMetricityTests(APISubdomainTestCase): def test_get_metricity_data(self): # Given - verified_at = "foo" + joined_at = "foo" total_messages = 1 total_blocks = 1 - self.mock_metricity_user(verified_at, total_messages, total_blocks) + self.mock_metricity_user(joined_at, total_messages, total_blocks) # When url = reverse('bot:user-metricity-data', args=[0], host='api') @@ -419,7 +419,7 @@ class UserMetricityTests(APISubdomainTestCase): # Then self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), { - "verified_at": verified_at, + "joined_at": joined_at, "total_messages": total_messages, "voice_banned": False, "activity_blocks": total_blocks @@ -455,12 +455,12 @@ class UserMetricityTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["voice_banned"], case["voice_banned"]) - def mock_metricity_user(self, verified_at, total_messages, total_blocks): + def mock_metricity_user(self, joined_at, total_messages, total_blocks): patcher = patch("pydis_site.apps.api.viewsets.bot.user.Metricity") self.metricity = patcher.start() self.addCleanup(patcher.stop) self.metricity = self.metricity.return_value.__enter__.return_value - self.metricity.user.return_value = dict(verified_at=verified_at) + self.metricity.user.return_value = dict(joined_at=joined_at) self.metricity.total_messages.return_value = total_messages self.metricity.total_message_blocks.return_value = total_blocks diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 79f90163..829e2694 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -109,7 +109,7 @@ class UserViewSet(ModelViewSet): #### Response format >>> { - ... "verified_at": "2020-10-06T21:54:23.540766", + ... "joined_at": "2020-10-06T21:54:23.540766", ... "total_messages": 2, ... "voice_banned": False, ... "activity_blocks": 1 -- cgit v1.2.3 From 51c57d3707c684bb908195f419e53f4ed164ba3b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 22 Feb 2021 08:23:04 +0200 Subject: Update nominations viewset GET and POST to make this working with 2-table system --- pydis_site/apps/api/viewsets/bot/nomination.py | 119 ++++++++++++++++++++----- 1 file changed, 96 insertions(+), 23 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index cf6e262f..8775515c 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -15,7 +15,8 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from pydis_site.apps.api.models.bot import Nomination -from pydis_site.apps.api.serializers import NominationSerializer +from pydis_site.apps.api.models.bot.nomination import NominationEntry +from pydis_site.apps.api.serializers import NominationEntrySerializer, NominationSerializer class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, GenericViewSet): @@ -29,7 +30,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge #### Query parameters - **active** `bool`: whether the nomination is still active - - **actor__id** `int`: snowflake of the user who nominated the user - **user__id** `int`: snowflake of the user who received the nomination - **ordering** `str`: comma-separated sequence of fields to order the returned results @@ -40,12 +40,18 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge ... { ... 'id': 1, ... 'active': false, - ... 'actor': 336843820513755157, - ... 'reason': 'They know how to explain difficult concepts', ... 'user': 336843820513755157, ... 'inserted_at': '2019-04-25T14:02:37.775587Z', ... 'end_reason': 'They were helpered after a staff-vote', - ... 'ended_at': '2019-04-26T15:12:22.123587Z' + ... 'ended_at': '2019-04-26T15:12:22.123587Z', + ... 'entries': [ + ... { + ... 'actor': 336843820513755157, + ... 'reason': 'They know how to explain difficult concepts', + ... 'inserted_at': '2019-04-25T14:02:37.775587Z' + ... } + ... ], + ... 'reviewed': true ... } ... ] @@ -59,12 +65,18 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge >>> { ... 'id': 1, ... 'active': true, - ... 'actor': 336843820513755157, - ... 'reason': 'They know how to explain difficult concepts', ... 'user': 336843820513755157, ... 'inserted_at': '2019-04-25T14:02:37.775587Z', ... 'end_reason': 'They were helpered after a staff-vote', - ... 'ended_at': '2019-04-26T15:12:22.123587Z' + ... 'ended_at': '2019-04-26T15:12:22.123587Z', + ... 'entries': [ + ... { + ... 'actor': 336843820513755157, + ... 'reason': 'They know how to explain difficult concepts', + ... 'inserted_at': '2019-04-25T14:02:37.775587Z' + ... } + ... ], + ... 'reviewed': false ... } ### Status codes @@ -75,8 +87,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge Create a new, active nomination returns the created nominations. The `user`, `reason` and `actor` fields are required and the `user` and `actor` need to know by the site. Providing other valid fields - is not allowed and invalid fields are ignored. A `user` is only - allowed one active nomination at a time. + is not allowed and invalid fields are ignored. If `user` already have + active nomination, new nomination entry will be created assigned to + active nomination. #### Request body >>> { @@ -91,7 +104,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge #### Status codes - 201: returned on success - 400: returned on failure for one of the following reasons: - - A user already has an active nomination; - The `user` or `actor` are unknown to the site; - The request contained a field that cannot be set at creation. @@ -148,10 +160,44 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge serializer_class = NominationSerializer queryset = Nomination.objects.all() filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) - filter_fields = ('user__id', 'actor__id', 'active') - frozen_fields = ('id', 'actor', 'inserted_at', 'user', 'ended_at') + filter_fields = ('user__id', 'active') + frozen_fields = ('id', 'inserted_at', 'user', 'ended_at') frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at') + def list(self, request: HttpRequest, *args, **kwargs) -> Response: + """ + DRF method for listing Nominations. + + Called by the Django Rest Framework in response to the corresponding HTTP request. + """ + queryset = self.filter_queryset(self.get_queryset()) + data = NominationSerializer(queryset, many=True).data + + for i, nomination in enumerate(data): + entries = NominationEntrySerializer( + NominationEntry.objects.filter(nomination_id=nomination["id"]), + many=True + ).data + data[i]["entries"] = entries + + return Response(data) + + def retrieve(self, request: HttpRequest, *args, **kwargs) -> Response: + """ + DRF method for retrieving a Nomination. + + Called by the Django Rest Framework in response to the corresponding HTTP request. + """ + nomination = self.get_object() + + data = NominationSerializer(nomination).data + data["entries"] = NominationEntrySerializer( + NominationEntry.objects.filter(nomination_id=nomination.id), + many=True + ).data + + return Response(data) + def create(self, request: HttpRequest, *args, **kwargs) -> Response: """ DRF method for creating a Nomination. @@ -163,19 +209,46 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge raise ValidationError({field: ['This field cannot be set at creation.']}) user_id = request.data.get("user") - if Nomination.objects.filter(active=True, user__id=user_id).exists(): - raise ValidationError({'active': ['There can only be one active nomination.']}) + nomination_filter = Nomination.objects.filter(active=True, user__id=user_id) + + if not nomination_filter.exists(): + serializer = NominationSerializer( + data=ChainMap( + request.data, + {"active": True} + ) + ) + serializer.is_valid(raise_exception=True) + nomination = Nomination.objects.create(**serializer.validated_data) - serializer = self.get_serializer( - data=ChainMap( - request.data, - {"active": True} + # Serializer truncate unnecessary data away + entry_serializer = NominationEntrySerializer( + data=ChainMap(request.data, {"nomination": nomination.id}) ) + entry_serializer.is_valid(raise_exception=True) + + entry = NominationEntry.objects.create(**entry_serializer.validated_data) + + data = NominationSerializer(nomination).data + data["entries"] = NominationEntrySerializer([entry], many=True).data + + headers = self.get_success_headers(data) + return Response(data, status=status.HTTP_201_CREATED, headers=headers) + + entry_serializer = NominationEntrySerializer( + data=ChainMap(request.data, {"nomination": nomination_filter[0].id}) ) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + entry_serializer.is_valid(raise_exception=True) + NominationEntry.objects.create(**entry_serializer.validated_data) + + data = NominationSerializer(nomination_filter[0]).data + data["entries"] = NominationEntrySerializer( + NominationEntry.objects.filter(nomination_id=nomination_filter[0].id), + many=True + ).data + + headers = self.get_success_headers(data) + return Response(data, status=status.HTTP_201_CREATED, headers=headers) def partial_update(self, request: HttpRequest, *args, **kwargs) -> Response: """ -- cgit v1.2.3 From e17da1f2e991710beaabd37701ce06a57f7b4a77 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 22 Feb 2021 09:01:18 +0200 Subject: Migrate PATCH request for 2-table nominations system --- pydis_site/apps/api/viewsets/bot/nomination.py | 70 ++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 10 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 8775515c..14dee9bc 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -112,18 +112,20 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge The PATCH route can be used for three distinct operations: 1. Updating the `reason` of `active` nomination; - 2. Ending an `active` nomination; - 3. Updating the `end_reason` or `reason` field of an `inactive` nomination. + 2. Updating `reviewed` field of `active` nomination. + 3. Ending an `active` nomination; + 4. Updating the `end_reason` or `reason` field of an `inactive` nomination. While the response format and status codes are the same for all three operations (see below), the request bodies vary depending on the operation. For all operations it holds that providing other valid fields is not allowed and invalid fields are ignored. - ### 1. Updating the `reason` of `active` nomination + ### 1. Updating the `reason` of `active` nomination. Actor field is required. #### Request body >>> { ... 'reason': 'He would make a great helper', + ... 'actor': 409107086526644234 ... } #### Response format @@ -134,7 +136,16 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge - 400: if a field in the request body is invalid or disallowed - 404: if an infraction with the given `id` could not be found - ### 2. Ending an `active` nomination + ### 2. Setting nomination `reviewed` + + #### Request body + >>> { + ... 'reviewed': True + ... } + + See operation 1 for the response format and status codes. + + ### 3. Ending an `active` nomination #### Request body >>> { @@ -144,11 +155,13 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge See operation 1 for the response format and status codes. - ### 3. Updating the `end_reason` or `reason` field of an `inactive` nomination. + ### 4. Updating the `end_reason` or `reason` field of an `inactive` nomination. + Actor field is required when updating reason. #### Request body >>> { ... 'reason': 'Updated reason for this nomination', + ... 'actor': 409107086526644234, ... 'end_reason': 'Updated end_reason for this nomination', ... } @@ -162,7 +175,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) filter_fields = ('user__id', 'active') frozen_fields = ('id', 'inserted_at', 'user', 'ended_at') - frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at') + frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at', 'reviewed') def list(self, request: HttpRequest, *args, **kwargs) -> Response: """ @@ -274,8 +287,20 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge {'end_reason': ["An active nomination can't have an end reason."]} ) + elif 'reviewed' in data: + # 2. We're setting nomination reviewed + if not instance.active: + raise ValidationError( + {'reviewed': 'This field cannot be set if nomination is inactive.'} + ) + + if 'active' in data: + raise ValidationError( + {'active': 'This field cannot be set same time than ending nomination.'} + ) + elif instance.active and not data['active']: - # 2. We're ending an active nomination. + # 3. We're ending an active nomination. if 'reason' in data: raise ValidationError( {'reason': ['This field cannot be set when ending a nomination.']} @@ -289,11 +314,36 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge instance.ended_at = timezone.now() elif 'active' in data: - # 3. The `active` field is only allowed when ending a nomination. + # 4. The `active` field is only allowed when ending a nomination. raise ValidationError( {'active': ['This field can only be used to end a nomination']} ) - serializer.save() + if 'reason' in request.data: + if 'actor' not in request.data: + raise ValidationError( + {'actor': 'This field is required when editing reason.'} + ) + + entry_filter = NominationEntry.objects.filter( + nomination_id=instance.id, + actor__id=request.data['actor'] + ) + + if not entry_filter.exists(): + raise ValidationError( + {'actor': "Actor don't exist or have not nominated user."} + ) + + entry = entry_filter[0] + entry.reason = request.data['reason'] + entry.save() + + nomination = serializer.save() + return_data = NominationSerializer(nomination).data + return_data["entries"] = NominationEntrySerializer( + NominationEntry.objects.filter(nomination_id=nomination.id), + many=True + ).data - return Response(serializer.data) + return Response(return_data) -- cgit v1.2.3 From 0ae69d9892db73029336e7b9ca95164dd80f828f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 22 Feb 2021 09:05:54 +0200 Subject: Disable creating multiple nomination entries of one nomination for one actor --- pydis_site/apps/api/viewsets/bot/nomination.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 14dee9bc..81fb43f7 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -252,6 +252,16 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge data=ChainMap(request.data, {"nomination": nomination_filter[0].id}) ) entry_serializer.is_valid(raise_exception=True) + + # Don't allow user creating many nomination entries for one nomination + if NominationEntry.objects.filter( + nomination_id=nomination_filter[0].id, + actor__id=entry_serializer.validated_data["actor"].id + ).exists(): + raise ValidationError( + {'actor': 'This actor have already created nomination entry for this nomination.'} + ) + NominationEntry.objects.create(**entry_serializer.validated_data) data = NominationSerializer(nomination_filter[0]).data -- cgit v1.2.3 From 698cbc17405a49fe42669fedd6054ab6c9008a4a Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 22 Feb 2021 09:37:12 +0200 Subject: Wrap validation errors to [] --- pydis_site/apps/api/viewsets/bot/nomination.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 81fb43f7..c4600425 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -259,7 +259,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge actor__id=entry_serializer.validated_data["actor"].id ).exists(): raise ValidationError( - {'actor': 'This actor have already created nomination entry for this nomination.'} + {'actor': ['This actor have already created nomination entry for this nomination.']} ) NominationEntry.objects.create(**entry_serializer.validated_data) @@ -301,12 +301,12 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge # 2. We're setting nomination reviewed if not instance.active: raise ValidationError( - {'reviewed': 'This field cannot be set if nomination is inactive.'} + {'reviewed': ['This field cannot be set if nomination is inactive.']} ) if 'active' in data: raise ValidationError( - {'active': 'This field cannot be set same time than ending nomination.'} + {'active': ['This field cannot be set same time than ending nomination.']} ) elif instance.active and not data['active']: @@ -332,7 +332,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge if 'reason' in request.data: if 'actor' not in request.data: raise ValidationError( - {'actor': 'This field is required when editing reason.'} + {'actor': ['This field is required when editing reason.']} ) entry_filter = NominationEntry.objects.filter( @@ -342,7 +342,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge if not entry_filter.exists(): raise ValidationError( - {'actor': "Actor don't exist or have not nominated user."} + {'actor': ["Actor don't exist or have not nominated user."]} ) entry = entry_filter[0] -- cgit v1.2.3 From 3907122c9e76f78bd7bfd07028e9eb79b43d65b3 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 22 Feb 2021 11:37:33 +0200 Subject: Small improvements in nomination viewset --- pydis_site/apps/api/viewsets/bot/nomination.py | 62 +++++++++++++------------- 1 file changed, 31 insertions(+), 31 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index c4600425..7820ca0d 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -14,8 +14,7 @@ from rest_framework.mixins import ( from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet -from pydis_site.apps.api.models.bot import Nomination -from pydis_site.apps.api.models.bot.nomination import NominationEntry +from pydis_site.apps.api.models.bot import Nomination, NominationEntry from pydis_site.apps.api.serializers import NominationEntrySerializer, NominationSerializer @@ -112,9 +111,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge The PATCH route can be used for three distinct operations: 1. Updating the `reason` of `active` nomination; - 2. Updating `reviewed` field of `active` nomination. - 3. Ending an `active` nomination; - 4. Updating the `end_reason` or `reason` field of an `inactive` nomination. + 2. Ending an `active` nomination; + 3. Updating the `end_reason` or `reason` field of an `inactive` nomination. + 4. Updating `reviewed` field of `active` nomination. While the response format and status codes are the same for all three operations (see below), the request bodies vary depending on the operation. For all operations it holds @@ -136,16 +135,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge - 400: if a field in the request body is invalid or disallowed - 404: if an infraction with the given `id` could not be found - ### 2. Setting nomination `reviewed` - - #### Request body - >>> { - ... 'reviewed': True - ... } - - See operation 1 for the response format and status codes. - - ### 3. Ending an `active` nomination + ### 2. Ending an `active` nomination #### Request body >>> { @@ -155,7 +145,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge See operation 1 for the response format and status codes. - ### 4. Updating the `end_reason` or `reason` field of an `inactive` nomination. + ### 3. Updating the `end_reason` or `reason` field of an `inactive` nomination. Actor field is required when updating reason. #### Request body @@ -167,6 +157,15 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge Note: The request body may contain either or both fields. + See operation 1 for the response format and status codes. + + ### 4. Setting nomination `reviewed` + + #### Request body + >>> { + ... 'reviewed': True + ... } + See operation 1 for the response format and status codes. """ @@ -297,21 +296,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge {'end_reason': ["An active nomination can't have an end reason."]} ) - elif 'reviewed' in data: - # 2. We're setting nomination reviewed - if not instance.active: - raise ValidationError( - {'reviewed': ['This field cannot be set if nomination is inactive.']} - ) - - if 'active' in data: - raise ValidationError( - {'active': ['This field cannot be set same time than ending nomination.']} - ) - elif instance.active and not data['active']: - # 3. We're ending an active nomination. - if 'reason' in data: + # 2. We're ending an active nomination. + if 'reason' in request.data: raise ValidationError( {'reason': ['This field cannot be set when ending a nomination.']} ) @@ -321,14 +308,27 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge {'end_reason': ['This field is required when ending a nomination.']} ) + if 'reviewed' in request.data: + raise ValidationError( + {'reviewed': ['This field cannot be set same time than ending nomination.']} + ) + instance.ended_at = timezone.now() elif 'active' in data: - # 4. The `active` field is only allowed when ending a nomination. + # 3. The `active` field is only allowed when ending a nomination. raise ValidationError( {'active': ['This field can only be used to end a nomination']} ) + # This is actually covered, but for some reason coverage don't think so. + elif 'reviewed' in request.data: # pragma: no cover + # 4. We're setting nomination reviewed + if not instance.active: + raise ValidationError( + {'reviewed': ['This field cannot be set if nomination is inactive.']} + ) + if 'reason' in request.data: if 'actor' not in request.data: raise ValidationError( -- cgit v1.2.3 From 3edb416ba86c62d62c0258d4c69f967e47765186 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Feb 2021 08:38:13 +0200 Subject: Simplify nominations viewset After moving entries to nomination serializer we can get rid from GET request handlers and let DRF handle this. Also PATCH and POST handlers got some simplification by removing manual entries setting. --- pydis_site/apps/api/viewsets/bot/nomination.py | 51 ++------------------------ 1 file changed, 3 insertions(+), 48 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 7820ca0d..e3e71ca2 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -176,40 +176,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge frozen_fields = ('id', 'inserted_at', 'user', 'ended_at') frozen_on_create = ('ended_at', 'end_reason', 'active', 'inserted_at', 'reviewed') - def list(self, request: HttpRequest, *args, **kwargs) -> Response: - """ - DRF method for listing Nominations. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - queryset = self.filter_queryset(self.get_queryset()) - data = NominationSerializer(queryset, many=True).data - - for i, nomination in enumerate(data): - entries = NominationEntrySerializer( - NominationEntry.objects.filter(nomination_id=nomination["id"]), - many=True - ).data - data[i]["entries"] = entries - - return Response(data) - - def retrieve(self, request: HttpRequest, *args, **kwargs) -> Response: - """ - DRF method for retrieving a Nomination. - - Called by the Django Rest Framework in response to the corresponding HTTP request. - """ - nomination = self.get_object() - - data = NominationSerializer(nomination).data - data["entries"] = NominationEntrySerializer( - NominationEntry.objects.filter(nomination_id=nomination.id), - many=True - ).data - - return Response(data) - def create(self, request: HttpRequest, *args, **kwargs) -> Response: """ DRF method for creating a Nomination. @@ -238,11 +204,9 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge data=ChainMap(request.data, {"nomination": nomination.id}) ) entry_serializer.is_valid(raise_exception=True) - - entry = NominationEntry.objects.create(**entry_serializer.validated_data) + NominationEntry.objects.create(**entry_serializer.validated_data) data = NominationSerializer(nomination).data - data["entries"] = NominationEntrySerializer([entry], many=True).data headers = self.get_success_headers(data) return Response(data, status=status.HTTP_201_CREATED, headers=headers) @@ -264,10 +228,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge NominationEntry.objects.create(**entry_serializer.validated_data) data = NominationSerializer(nomination_filter[0]).data - data["entries"] = NominationEntrySerializer( - NominationEntry.objects.filter(nomination_id=nomination_filter[0].id), - many=True - ).data headers = self.get_success_headers(data) return Response(data, status=status.HTTP_201_CREATED, headers=headers) @@ -349,11 +309,6 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge entry.reason = request.data['reason'] entry.save() - nomination = serializer.save() - return_data = NominationSerializer(nomination).data - return_data["entries"] = NominationEntrySerializer( - NominationEntry.objects.filter(nomination_id=nomination.id), - many=True - ).data + serializer.save() - return Response(return_data) + return Response(serializer.data) -- cgit v1.2.3 From 54eca6da46d83d8872746b89b95a22f7cf0c2b52 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 5 Mar 2021 07:48:32 +0200 Subject: Fix grammar of nomination endpoints documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Leon Sandøy Co-authored-by: Joe Banks --- pydis_site/apps/api/viewsets/bot/nomination.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index e3e71ca2..c208df46 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -86,8 +86,8 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge Create a new, active nomination returns the created nominations. The `user`, `reason` and `actor` fields are required and the `user` and `actor` need to know by the site. Providing other valid fields - is not allowed and invalid fields are ignored. If `user` already have - active nomination, new nomination entry will be created assigned to + is not allowed and invalid fields are ignored. If `user` already has an + active nomination, a new nomination entry will be created and assigned as the active nomination. #### Request body @@ -119,7 +119,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge below), the request bodies vary depending on the operation. For all operations it holds that providing other valid fields is not allowed and invalid fields are ignored. - ### 1. Updating the `reason` of `active` nomination. Actor field is required. + ### 1. Updating the `reason` of `active` nomination. The `actor` field is required. #### Request body >>> { @@ -199,7 +199,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge serializer.is_valid(raise_exception=True) nomination = Nomination.objects.create(**serializer.validated_data) - # Serializer truncate unnecessary data away + # The serializer will truncate and get rid of excessive data entry_serializer = NominationEntrySerializer( data=ChainMap(request.data, {"nomination": nomination.id}) ) @@ -283,7 +283,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge # This is actually covered, but for some reason coverage don't think so. elif 'reviewed' in request.data: # pragma: no cover - # 4. We're setting nomination reviewed + # 4. We are altering the reviewed state of the nomination. if not instance.active: raise ValidationError( {'reviewed': ['This field cannot be set if nomination is inactive.']} -- cgit v1.2.3 From 72693a6b72332c1ca9411f73ae14d395c5dd8933 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 5 Mar 2021 07:49:46 +0200 Subject: Replace double quotes with single quotes Co-authored-by: Joe Banks --- pydis_site/apps/api/viewsets/bot/nomination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index c208df46..ef7d1dab 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -302,7 +302,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge if not entry_filter.exists(): raise ValidationError( - {'actor': ["Actor don't exist or have not nominated user."]} + {'actor': ['Actor don't exist or have not nominated user.']} ) entry = entry_filter[0] -- cgit v1.2.3 From 9075231a5fd68986b0845327d6a2bffe70447fc8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 5 Mar 2021 08:18:17 +0200 Subject: Use double quotes instead apostrophe because string contain "don't" --- pydis_site/apps/api/viewsets/bot/nomination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index ef7d1dab..c208df46 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -302,7 +302,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge if not entry_filter.exists(): raise ValidationError( - {'actor': ['Actor don't exist or have not nominated user.']} + {'actor': ["Actor don't exist or have not nominated user."]} ) entry = entry_filter[0] -- cgit v1.2.3 From d8751ad37ed5a493554575ea3adb264def342664 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 5 Mar 2021 08:23:28 +0200 Subject: Fix grammar of error messages and change tests to match with changes --- pydis_site/apps/api/tests/test_nominations.py | 8 ++++---- pydis_site/apps/api/viewsets/bot/nomination.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 1daa6f3f..c07679c5 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -78,7 +78,7 @@ class CreationTests(APISubdomainTestCase): response2 = self.client.post(url, data=data) self.assertEqual(response2.status_code, 400) self.assertEqual(response2.json(), { - 'actor': ['This actor have already created nomination entry for this nomination.'] + 'actor': ['This actor has already endorsed this nomination.'] }) def test_returns_400_for_missing_user(self): @@ -498,7 +498,7 @@ class NominationTests(APISubdomainTestCase): response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { - 'reviewed': ['This field cannot be set if nomination is inactive.'] + 'reviewed': ['This field cannot be set if the nomination is inactive.'] }) def test_patch_nomination_set_reviewed_and_end(self): @@ -508,7 +508,7 @@ class NominationTests(APISubdomainTestCase): response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { - 'reviewed': ['This field cannot be set same time than ending nomination.'] + 'reviewed': ['This field cannot be set while you are ending a nomination.'] }) def test_modifying_reason_without_actor(self): @@ -518,7 +518,7 @@ class NominationTests(APISubdomainTestCase): response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { - 'actor': ['This field is required when editing reason.'] + 'actor': ['This field is required when editing the reason.'] }) def test_modifying_reason_with_unknown_actor(self): diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index c208df46..451264f4 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -222,7 +222,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge actor__id=entry_serializer.validated_data["actor"].id ).exists(): raise ValidationError( - {'actor': ['This actor have already created nomination entry for this nomination.']} + {'actor': ['This actor has already endorsed this nomination.']} ) NominationEntry.objects.create(**entry_serializer.validated_data) @@ -270,7 +270,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge if 'reviewed' in request.data: raise ValidationError( - {'reviewed': ['This field cannot be set same time than ending nomination.']} + {'reviewed': ['This field cannot be set while you are ending a nomination.']} ) instance.ended_at = timezone.now() @@ -286,13 +286,13 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge # 4. We are altering the reviewed state of the nomination. if not instance.active: raise ValidationError( - {'reviewed': ['This field cannot be set if nomination is inactive.']} + {'reviewed': ['This field cannot be set if the nomination is inactive.']} ) if 'reason' in request.data: if 'actor' not in request.data: raise ValidationError( - {'actor': ['This field is required when editing reason.']} + {'actor': ['This field is required when editing the reason.']} ) entry_filter = NominationEntry.objects.filter( -- cgit v1.2.3 From ce5a082458d8cc1433ce623d3b666fb6e0f82672 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:11:56 +0200 Subject: Change as -> to in nomination viewset docs --- pydis_site/apps/api/viewsets/bot/nomination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 451264f4..9b9aa280 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -87,7 +87,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge The `user`, `reason` and `actor` fields are required and the `user` and `actor` need to know by the site. Providing other valid fields is not allowed and invalid fields are ignored. If `user` already has an - active nomination, a new nomination entry will be created and assigned as the + active nomination, a new nomination entry will be created and assigned to the active nomination. #### Request body -- cgit v1.2.3 From 9202d92d3cb27111eaf5d3cc1aee859f9cbe8918 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:12:56 +0200 Subject: Fix grammar of nomination viewset command about single entry for user --- pydis_site/apps/api/viewsets/bot/nomination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 9b9aa280..0792a2a6 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -216,7 +216,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge ) entry_serializer.is_valid(raise_exception=True) - # Don't allow user creating many nomination entries for one nomination + # Don't allow a user to create many nomination entries in a single nomination if NominationEntry.objects.filter( nomination_id=nomination_filter[0].id, actor__id=entry_serializer.validated_data["actor"].id -- cgit v1.2.3 From 48eff5d27715acd658f6284f4d4e1cdbbf71bee6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:13:50 +0200 Subject: Fix grammar of unknown actor error and change tests --- pydis_site/apps/api/tests/test_nominations.py | 2 +- pydis_site/apps/api/viewsets/bot/nomination.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site/apps/api/viewsets/bot') diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index c07679c5..9cefbd8f 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -528,5 +528,5 @@ class NominationTests(APISubdomainTestCase): response = self.client.patch(url, data=data) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { - 'actor': ["Actor don't exist or have not nominated user."] + 'actor': ["The actor doesn't exist or has not nominated the user."] }) diff --git a/pydis_site/apps/api/viewsets/bot/nomination.py b/pydis_site/apps/api/viewsets/bot/nomination.py index 0792a2a6..144daab0 100644 --- a/pydis_site/apps/api/viewsets/bot/nomination.py +++ b/pydis_site/apps/api/viewsets/bot/nomination.py @@ -302,7 +302,7 @@ class NominationViewSet(CreateModelMixin, RetrieveModelMixin, ListModelMixin, Ge if not entry_filter.exists(): raise ValidationError( - {'actor': ["Actor don't exist or have not nominated user."]} + {'actor': ["The actor doesn't exist or has not nominated the user."]} ) entry = entry_filter[0] -- cgit v1.2.3