prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  [FR] :checkered_flag: Simple API endpoint to change user metadata
  ### Please verify that this feature request has NOT been suggested before.
  
  - [X] I checked and didn't find similar feature request
  
  ### Problem statement
  
  we currently provide an API endpoint for editing user data via a form. Fetching and changing endpoints are different and take different parameters.
  
  ### Suggested solution
  
  I think it would be better to provide a RetrieveUpdateAPI endpoint under `/api/user/me` (somewhat a convention). This endpoint could provide and patch all the metadata for a user.
  
  ### Describe alternatives you've considered
  
  N/A
  
  ### Examples of other systems
  
  _No response_
  
  ### Do you want to develop this?
  
  - [X] I want to develop this.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `InvenTree/users/api.py`
  Content:
  ```
  1 """DRF API definition for the 'users' app"""
  2 
  3 from django.contrib.auth.models import User
  4 from django.core.exceptions import ObjectDoesNotExist
  5 from django.urls import include, path, re_path
  6 
  7 from django_filters.rest_framework import DjangoFilterBackend
  8 from rest_framework import filters, permissions, status
  9 from rest_framework.authtoken.models import Token
  10 from rest_framework.response import Response
  11 from rest_framework.views import APIView
  12 
  13 from InvenTree.mixins import ListAPI, RetrieveAPI
  14 from InvenTree.serializers import UserSerializer
  15 from users.models import Owner, RuleSet, check_user_role
  16 from users.serializers import OwnerSerializer
  17 
  18 
  19 class OwnerList(ListAPI):
  20     """List API endpoint for Owner model.
  21 
  22     Cannot create.
  23     """
  24 
  25     queryset = Owner.objects.all()
  26     serializer_class = OwnerSerializer
  27 
  28     def filter_queryset(self, queryset):
  29         """Implement text search for the "owner" model.
  30 
  31         Note that an "owner" can be either a group, or a user,
  32         so we cannot do a direct text search.
  33 
  34         A "hack" here is to post-process the queryset and simply
  35         remove any values which do not match.
  36 
  37         It is not necessarily "efficient" to do it this way,
  38         but until we determine a better way, this is what we have...
  39         """
  40         search_term = str(self.request.query_params.get('search', '')).lower()
  41 
  42         queryset = super().filter_queryset(queryset)
  43 
  44         if not search_term:
  45             return queryset
  46 
  47         results = []
  48 
  49         # Extract search term f
  50 
  51         for result in queryset.all():
  52             if search_term in result.name().lower():
  53                 results.append(result)
  54 
  55         return results
  56 
  57 
  58 class OwnerDetail(RetrieveAPI):
  59     """Detail API endpoint for Owner model.
  60 
  61     Cannot edit or delete
  62     """
  63 
  64     queryset = Owner.objects.all()
  65     serializer_class = OwnerSerializer
  66 
  67 
  68 class RoleDetails(APIView):
  69     """API endpoint which lists the available role permissions for the current user.
  70 
  71     (Requires authentication)
  72     """
  73 
  74     permission_classes = [
  75         permissions.IsAuthenticated
  76     ]
  77 
  78     def get(self, request, *args, **kwargs):
  79         """Return the list of roles / permissions available to the current user"""
  80         user = request.user
  81 
  82         roles = {}
  83 
  84         for ruleset in RuleSet.RULESET_CHOICES:
  85 
  86             role, text = ruleset
  87 
  88             permissions = []
  89 
  90             for permission in RuleSet.RULESET_PERMISSIONS:
  91                 if check_user_role(user, role, permission):
  92 
  93                     permissions.append(permission)
  94 
  95             if len(permissions) > 0:
  96                 roles[role] = permissions
  97             else:
  98                 roles[role] = None  # pragma: no cover
  99 
  100         data = {
  101             'user': user.pk,
  102             'username': user.username,
  103             'roles': roles,
  104             'is_staff': user.is_staff,
  105             'is_superuser': user.is_superuser,
  106         }
  107 
  108         return Response(data)
  109 
  110 
  111 class UserDetail(RetrieveAPI):
  112     """Detail endpoint for a single user."""
  113 
  114     queryset = User.objects.all()
  115     serializer_class = UserSerializer
  116     permission_classes = (permissions.IsAuthenticated,)
  117 
  118 
  119 class UserList(ListAPI):
  120     """List endpoint for detail on all users."""
  121 
  122     queryset = User.objects.all()
  123     serializer_class = UserSerializer
  124     permission_classes = (permissions.IsAuthenticated,)
  125 
  126     filter_backends = [
  127         DjangoFilterBackend,
  128         filters.SearchFilter,
  129     ]
  130 
  131     search_fields = [
  132         'first_name',
  133         'last_name',
  134         'username',
  135     ]
  136 
  137 
  138 class GetAuthToken(APIView):
  139     """Return authentication token for an authenticated user."""
  140 
  141     permission_classes = [
  142         permissions.IsAuthenticated,
  143     ]
  144 
  145     def get(self, request, *args, **kwargs):
  146         """Return an API token if the user is authenticated
  147 
  148         - If the user already has a token, return it
  149         - Otherwise, create a new token
  150         """
  151         if request.user.is_authenticated:
  152             # Get the user token (or create one if it does not exist)
  153             token, created = Token.objects.get_or_create(user=request.user)
  154             return Response({
  155                 'token': token.key,
  156             })
  157 
  158     def delete(self, request):
  159         """User has requested deletion of API token"""
  160         try:
  161             request.user.auth_token.delete()
  162             return Response({"success": "Successfully logged out."},
  163                             status=status.HTTP_202_ACCEPTED)
  164         except (AttributeError, ObjectDoesNotExist):
  165             return Response({"error": "Bad request"},
  166                             status=status.HTTP_400_BAD_REQUEST)
  167 
  168 
  169 user_urls = [
  170 
  171     re_path(r'roles/?$', RoleDetails.as_view(), name='api-user-roles'),
  172     re_path(r'token/?$', GetAuthToken.as_view(), name='api-token'),
  173 
  174     re_path(r'^owner/', include([
  175         path('<int:pk>/', OwnerDetail.as_view(), name='api-owner-detail'),
  176         re_path(r'^.*$', OwnerList.as_view(), name='api-owner-list'),
  177     ])),
  178 
  179     re_path(r'^(?P<pk>[0-9]+)/?$', UserDetail.as_view(), name='user-detail'),
  180     path('', UserList.as_view()),
  181 ]
  182
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/InvenTree/users/api.py b/InvenTree/users/api.py
  --- a/InvenTree/users/api.py
  +++ b/InvenTree/users/api.py
  @@ -10,7 +10,7 @@
   from rest_framework.response import Response
   from rest_framework.views import APIView
   
  -from InvenTree.mixins import ListAPI, RetrieveAPI
  +from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateAPI
   from InvenTree.serializers import UserSerializer
   from users.models import Owner, RuleSet, check_user_role
   from users.serializers import OwnerSerializer
  @@ -116,6 +116,14 @@
       permission_classes = (permissions.IsAuthenticated,)
   
   
  +class MeUserDetail(RetrieveUpdateAPI, UserDetail):
  +    """Detail endpoint for current user."""
  +
  +    def get_object(self):
  +        """Always return the current user object"""
  +        return self.request.user
  +
  +
   class UserList(ListAPI):
       """List endpoint for detail on all users."""
   
  @@ -170,6 +178,7 @@
   
       re_path(r'roles/?$', RoleDetails.as_view(), name='api-user-roles'),
       re_path(r'token/?$', GetAuthToken.as_view(), name='api-token'),
  +    re_path(r'^me/', MeUserDetail.as_view(), name='api-user-me'),
   
       re_path(r'^owner/', include([
           path('<int:pk>/', OwnerDetail.as_view(), name='api-owner-detail'),
  
verification_info: {"golden_diff": "diff --git a/InvenTree/users/api.py b/InvenTree/users/api.py\n--- a/InvenTree/users/api.py\n+++ b/InvenTree/users/api.py\n@@ -10,7 +10,7 @@\n from rest_framework.response import Response\n from rest_framework.views import APIView\n \n-from InvenTree.mixins import ListAPI, RetrieveAPI\n+from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateAPI\n from InvenTree.serializers import UserSerializer\n from users.models import Owner, RuleSet, check_user_role\n from users.serializers import OwnerSerializer\n@@ -116,6 +116,14 @@\n     permission_classes = (permissions.IsAuthenticated,)\n \n \n+class MeUserDetail(RetrieveUpdateAPI, UserDetail):\n+    \"\"\"Detail endpoint for current user.\"\"\"\n+\n+    def get_object(self):\n+        \"\"\"Always return the current user object\"\"\"\n+        return self.request.user\n+\n+\n class UserList(ListAPI):\n     \"\"\"List endpoint for detail on all users.\"\"\"\n \n@@ -170,6 +178,7 @@\n \n     re_path(r'roles/?$', RoleDetails.as_view(), name='api-user-roles'),\n     re_path(r'token/?$', GetAuthToken.as_view(), name='api-token'),\n+    re_path(r'^me/', MeUserDetail.as_view(), name='api-user-me'),\n \n     re_path(r'^owner/', include([\n         path('<int:pk>/', OwnerDetail.as_view(), name='api-owner-detail'),\n", "issue": "[FR] :checkered_flag: Simple API endpoint to change user metadata\n### Please verify that this feature request has NOT been suggested before.\n\n- [X] I checked and didn't find similar feature request\n\n### Problem statement\n\nwe currently provide an API endpoint for editing user data via a form. Fetching and changing endpoints are different and take different parameters.\n\n### Suggested solution\n\nI think it would be better to provide a RetrieveUpdateAPI endpoint under `/api/user/me` (somewhat a convention). This endpoint could provide and patch all the metadata for a user.\n\n### Describe alternatives you've considered\n\nN/A\n\n### Examples of other systems\n\n_No response_\n\n### Do you want to develop this?\n\n- [X] I want to develop this.\n", "before_files": [{"content": "\"\"\"DRF API definition for the 'users' app\"\"\"\n\nfrom django.contrib.auth.models import User\nfrom django.core.exceptions import ObjectDoesNotExist\nfrom django.urls import include, path, re_path\n\nfrom django_filters.rest_framework import DjangoFilterBackend\nfrom rest_framework import filters, permissions, status\nfrom rest_framework.authtoken.models import Token\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\nfrom InvenTree.mixins import ListAPI, RetrieveAPI\nfrom InvenTree.serializers import UserSerializer\nfrom users.models import Owner, RuleSet, check_user_role\nfrom users.serializers import OwnerSerializer\n\n\nclass OwnerList(ListAPI):\n    \"\"\"List API endpoint for Owner model.\n\n    Cannot create.\n    \"\"\"\n\n    queryset = Owner.objects.all()\n    serializer_class = OwnerSerializer\n\n    def filter_queryset(self, queryset):\n        \"\"\"Implement text search for the \"owner\" model.\n\n        Note that an \"owner\" can be either a group, or a user,\n        so we cannot do a direct text search.\n\n        A \"hack\" here is to post-process the queryset and simply\n        remove any values which do not match.\n\n        It is not necessarily \"efficient\" to do it this way,\n        but until we determine a better way, this is what we have...\n        \"\"\"\n        search_term = str(self.request.query_params.get('search', '')).lower()\n\n        queryset = super().filter_queryset(queryset)\n\n        if not search_term:\n            return queryset\n\n        results = []\n\n        # Extract search term f\n\n        for result in queryset.all():\n            if search_term in result.name().lower():\n                results.append(result)\n\n        return results\n\n\nclass OwnerDetail(RetrieveAPI):\n    \"\"\"Detail API endpoint for Owner model.\n\n    Cannot edit or delete\n    \"\"\"\n\n    queryset = Owner.objects.all()\n    serializer_class = OwnerSerializer\n\n\nclass RoleDetails(APIView):\n    \"\"\"API endpoint which lists the available role permissions for the current user.\n\n    (Requires authentication)\n    \"\"\"\n\n    permission_classes = [\n        permissions.IsAuthenticated\n    ]\n\n    def get(self, request, *args, **kwargs):\n        \"\"\"Return the list of roles / permissions available to the current user\"\"\"\n        user = request.user\n\n        roles = {}\n\n        for ruleset in RuleSet.RULESET_CHOICES:\n\n            role, text = ruleset\n\n            permissions = []\n\n            for permission in RuleSet.RULESET_PERMISSIONS:\n                if check_user_role(user, role, permission):\n\n                    permissions.append(permission)\n\n            if len(permissions) > 0:\n                roles[role] = permissions\n            else:\n                roles[role] = None  # pragma: no cover\n\n        data = {\n            'user': user.pk,\n            'username': user.username,\n            'roles': roles,\n            'is_staff': user.is_staff,\n            'is_superuser': user.is_superuser,\n        }\n\n        return Response(data)\n\n\nclass UserDetail(RetrieveAPI):\n    \"\"\"Detail endpoint for a single user.\"\"\"\n\n    queryset = User.objects.all()\n    serializer_class = UserSerializer\n    permission_classes = (permissions.IsAuthenticated,)\n\n\nclass UserList(ListAPI):\n    \"\"\"List endpoint for detail on all users.\"\"\"\n\n    queryset = User.objects.all()\n    serializer_class = UserSerializer\n    permission_classes = (permissions.IsAuthenticated,)\n\n    filter_backends = [\n        DjangoFilterBackend,\n        filters.SearchFilter,\n    ]\n\n    search_fields = [\n        'first_name',\n        'last_name',\n        'username',\n    ]\n\n\nclass GetAuthToken(APIView):\n    \"\"\"Return authentication token for an authenticated user.\"\"\"\n\n    permission_classes = [\n        permissions.IsAuthenticated,\n    ]\n\n    def get(self, request, *args, **kwargs):\n        \"\"\"Return an API token if the user is authenticated\n\n        - If the user already has a token, return it\n        - Otherwise, create a new token\n        \"\"\"\n        if request.user.is_authenticated:\n            # Get the user token (or create one if it does not exist)\n            token, created = Token.objects.get_or_create(user=request.user)\n            return Response({\n                'token': token.key,\n            })\n\n    def delete(self, request):\n        \"\"\"User has requested deletion of API token\"\"\"\n        try:\n            request.user.auth_token.delete()\n            return Response({\"success\": \"Successfully logged out.\"},\n                            status=status.HTTP_202_ACCEPTED)\n        except (AttributeError, ObjectDoesNotExist):\n            return Response({\"error\": \"Bad request\"},\n                            status=status.HTTP_400_BAD_REQUEST)\n\n\nuser_urls = [\n\n    re_path(r'roles/?$', RoleDetails.as_view(), name='api-user-roles'),\n    re_path(r'token/?$', GetAuthToken.as_view(), name='api-token'),\n\n    re_path(r'^owner/', include([\n        path('<int:pk>/', OwnerDetail.as_view(), name='api-owner-detail'),\n        re_path(r'^.*$', OwnerList.as_view(), name='api-owner-list'),\n    ])),\n\n    re_path(r'^(?P<pk>[0-9]+)/?$', UserDetail.as_view(), name='user-detail'),\n    path('', UserList.as_view()),\n]\n", "path": "InvenTree/users/api.py"}], "after_files": [{"content": "\"\"\"DRF API definition for the 'users' app\"\"\"\n\nfrom django.contrib.auth.models import User\nfrom django.core.exceptions import ObjectDoesNotExist\nfrom django.urls import include, path, re_path\n\nfrom django_filters.rest_framework import DjangoFilterBackend\nfrom rest_framework import filters, permissions, status\nfrom rest_framework.authtoken.models import Token\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\nfrom InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateAPI\nfrom InvenTree.serializers import UserSerializer\nfrom users.models import Owner, RuleSet, check_user_role\nfrom users.serializers import OwnerSerializer\n\n\nclass OwnerList(ListAPI):\n    \"\"\"List API endpoint for Owner model.\n\n    Cannot create.\n    \"\"\"\n\n    queryset = Owner.objects.all()\n    serializer_class = OwnerSerializer\n\n    def filter_queryset(self, queryset):\n        \"\"\"Implement text search for the \"owner\" model.\n\n        Note that an \"owner\" can be either a group, or a user,\n        so we cannot do a direct text search.\n\n        A \"hack\" here is to post-process the queryset and simply\n        remove any values which do not match.\n\n        It is not necessarily \"efficient\" to do it this way,\n        but until we determine a better way, this is what we have...\n        \"\"\"\n        search_term = str(self.request.query_params.get('search', '')).lower()\n\n        queryset = super().filter_queryset(queryset)\n\n        if not search_term:\n            return queryset\n\n        results = []\n\n        # Extract search term f\n\n        for result in queryset.all():\n            if search_term in result.name().lower():\n                results.append(result)\n\n        return results\n\n\nclass OwnerDetail(RetrieveAPI):\n    \"\"\"Detail API endpoint for Owner model.\n\n    Cannot edit or delete\n    \"\"\"\n\n    queryset = Owner.objects.all()\n    serializer_class = OwnerSerializer\n\n\nclass RoleDetails(APIView):\n    \"\"\"API endpoint which lists the available role permissions for the current user.\n\n    (Requires authentication)\n    \"\"\"\n\n    permission_classes = [\n        permissions.IsAuthenticated\n    ]\n\n    def get(self, request, *args, **kwargs):\n        \"\"\"Return the list of roles / permissions available to the current user\"\"\"\n        user = request.user\n\n        roles = {}\n\n        for ruleset in RuleSet.RULESET_CHOICES:\n\n            role, text = ruleset\n\n            permissions = []\n\n            for permission in RuleSet.RULESET_PERMISSIONS:\n                if check_user_role(user, role, permission):\n\n                    permissions.append(permission)\n\n            if len(permissions) > 0:\n                roles[role] = permissions\n            else:\n                roles[role] = None  # pragma: no cover\n\n        data = {\n            'user': user.pk,\n            'username': user.username,\n            'roles': roles,\n            'is_staff': user.is_staff,\n            'is_superuser': user.is_superuser,\n        }\n\n        return Response(data)\n\n\nclass UserDetail(RetrieveAPI):\n    \"\"\"Detail endpoint for a single user.\"\"\"\n\n    queryset = User.objects.all()\n    serializer_class = UserSerializer\n    permission_classes = (permissions.IsAuthenticated,)\n\n\nclass MeUserDetail(RetrieveUpdateAPI, UserDetail):\n    \"\"\"Detail endpoint for current user.\"\"\"\n\n    def get_object(self):\n        \"\"\"Always return the current user object\"\"\"\n        return self.request.user\n\n\nclass UserList(ListAPI):\n    \"\"\"List endpoint for detail on all users.\"\"\"\n\n    queryset = User.objects.all()\n    serializer_class = UserSerializer\n    permission_classes = (permissions.IsAuthenticated,)\n\n    filter_backends = [\n        DjangoFilterBackend,\n        filters.SearchFilter,\n    ]\n\n    search_fields = [\n        'first_name',\n        'last_name',\n        'username',\n    ]\n\n\nclass GetAuthToken(APIView):\n    \"\"\"Return authentication token for an authenticated user.\"\"\"\n\n    permission_classes = [\n        permissions.IsAuthenticated,\n    ]\n\n    def get(self, request, *args, **kwargs):\n        \"\"\"Return an API token if the user is authenticated\n\n        - If the user already has a token, return it\n        - Otherwise, create a new token\n        \"\"\"\n        if request.user.is_authenticated:\n            # Get the user token (or create one if it does not exist)\n            token, created = Token.objects.get_or_create(user=request.user)\n            return Response({\n                'token': token.key,\n            })\n\n    def delete(self, request):\n        \"\"\"User has requested deletion of API token\"\"\"\n        try:\n            request.user.auth_token.delete()\n            return Response({\"success\": \"Successfully logged out.\"},\n                            status=status.HTTP_202_ACCEPTED)\n        except (AttributeError, ObjectDoesNotExist):\n            return Response({\"error\": \"Bad request\"},\n                            status=status.HTTP_400_BAD_REQUEST)\n\n\nuser_urls = [\n\n    re_path(r'roles/?$', RoleDetails.as_view(), name='api-user-roles'),\n    re_path(r'token/?$', GetAuthToken.as_view(), name='api-token'),\n    re_path(r'^me/', MeUserDetail.as_view(), name='api-user-me'),\n\n    re_path(r'^owner/', include([\n        path('<int:pk>/', OwnerDetail.as_view(), name='api-owner-detail'),\n        re_path(r'^.*$', OwnerList.as_view(), name='api-owner-list'),\n    ])),\n\n    re_path(r'^(?P<pk>[0-9]+)/?$', UserDetail.as_view(), name='user-detail'),\n    path('', UserList.as_view()),\n]\n", "path": "InvenTree/users/api.py"}]}