Repository: python-discord/site

Issue:
Add an endpoint to fetch user information from a username.
We'd need this endpoint to be able to implement features requested in [this](https://github.com/python-discord/bot/issues/1757) bot issue.

```py
from django.shortcuts import get_object_or_404

    @action(
        detail=False,
        methods=["GET"],
        url_path=r"username/(?P<name>[^#]{,32})#(?P<discriminator>[0-9]{4})"
    )
    def get_by_username(self, request, name, discriminator):
        user = get_object_or_404(User, name=name, discriminator=discriminator)
        return Response(UserSerializer(user).data, status=status.HTTP_200_OK)
```

This addition to the user view set seems to make GET requests to URLs like `bot/users/username/Qwerty#0001` work , but there might be a more ideal way to do this.


Discussion:
I realized that the infractions viewset allows filtering by fields.

Adding:
```py
filter_backends = (DjangoFilterBackend,)
filter_fields = ('name', 'discriminator')
```
To `UserViewSet` and then calling
```py
await ctx.bot.api_client.get("bot/users", params=dict(name="Qwerty", discriminator=1))
```
Allowed fetching the user data by name and discriminator.

I think the implementation of this is simpler, but it returns a nested dictionary with a list of user dictionaries under "results".
I'm fine with either approach, but someone else might have a stronger opinion.
It's probably possible to retrieve it as a simple list as the infractions endpoint does, but this is where my superficial django knowledge ends.

EDIT: Courtesy of @ChrisLovering, it's done with `pagination_class = LimitOffsetPaginationExtended`
Wouldn't this be more appropriate for python-discord/API?



> Wouldn't this be more appropriate for python-discord/API?

We don't have an "API" repository, and this is a site endpoint for which code would need to be added in this repository.
> 
> 
> 
> > Wouldn't this be more appropriate for python-discord/API?
> 
> We don't have an "API" repository, and this is a site endpoint for which code would need to be added in this repository.

https://github.com/python-discord/api
@Kronifer The new API is not live at the moment, and there's no feature freeze on this repo.
I agree with the approach suggested by @mbaruh. Reusing DRF's builtin logic for this allows us to keep custom code (including tests, docs, etc.) to a minimum.

Resolving patch:
diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py
--- a/pydis_site/apps/api/viewsets/bot/user.py
+++ b/pydis_site/apps/api/viewsets/bot/user.py
@@ -2,6 +2,7 @@
 from collections import OrderedDict
 
 from django.db.models import Q
+from django_filters.rest_framework import DjangoFilterBackend
 from rest_framework import status
 from rest_framework.decorators import action
 from rest_framework.pagination import PageNumberPagination
@@ -77,6 +78,8 @@ class UserViewSet(ModelViewSet):
     ... }
 
     #### Optional Query Parameters
+    - username: username to search for
+    - discriminator: discriminator to search for
     - page_size: number of Users in one page, defaults to 10,000
     - page: page number
 
@@ -233,6 +236,8 @@ class UserViewSet(ModelViewSet):
     serializer_class = UserSerializer
     queryset = User.objects.all().order_by("id")
     pagination_class = UserListPagination
+    filter_backends = (DjangoFilterBackend,)
+    filter_fields = ('name', 'discriminator')
 
     def get_serializer(self, *args, **kwargs) -> ModelSerializer:
         """Set Serializer many attribute to True if request body contains a list."""


Test patch:
diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py
--- a/pydis_site/apps/api/tests/test_users.py
+++ b/pydis_site/apps/api/tests/test_users.py
@@ -1,3 +1,4 @@
+import random
 from unittest.mock import Mock, patch
 
 from django.urls import reverse
@@ -520,3 +521,45 @@ def mock_no_metricity_user(self):
         self.metricity.total_messages.side_effect = NotFoundError()
         self.metricity.total_message_blocks.side_effect = NotFoundError()
         self.metricity.top_channel_activity.side_effect = NotFoundError()
+
+
+class UserViewSetTests(AuthenticatedAPITestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.searched_user = User.objects.create(
+            id=12095219,
+            name=f"Test user {random.randint(100, 1000)}",
+            discriminator=random.randint(1, 9999),
+            in_guild=True,
+        )
+        cls.other_user = User.objects.create(
+            id=18259125,
+            name=f"Test user {random.randint(100, 1000)}",
+            discriminator=random.randint(1, 9999),
+            in_guild=True,
+        )
+
+    def test_search_lookup_of_wanted_user(self) -> None:
+        """Searching a user by name and discriminator should return that user."""
+        url = reverse('api:bot:user-list')
+        params = {
+            'username': self.searched_user.name,
+            'discriminator': self.searched_user.discriminator,
+        }
+        response = self.client.get(url, params)
+        result = response.json()
+        self.assertEqual(result['count'], 1)
+        [user] = result['results']
+        self.assertEqual(user['id'], self.searched_user.id)
+
+    def test_search_lookup_of_unknown_user(self) -> None:
+        """Searching an unknown user should return no results."""
+        url = reverse('api:bot:user-list')
+        params = {
+            'username': "f-string enjoyer",
+            'discriminator': 1245,
+        }
+        response = self.client.get(url, params)
+        result = response.json()
+        self.assertEqual(result['count'], 0)
+        self.assertEqual(result['results'], [])