mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 04:25:42 +00:00
[PUI] User settings panel updates (#7944)
* Simplify user theme settings * Cleanup * Fix permission on user list endpoint * Update AccountDetailPanel to use modal form * Update components * UI updates * Implement default colors * Display more user details (read only) * Add specific "MeUserSerializer" - Prevent certain attributes from being adjusted * Add <YesNoUndefinedButton> * Allow role checks to be bypassed for a given view - Override the 'get_permission_model' attribute with None * Enable 'GET' metadata - Required for extracting field information even if we only have 'read' permissions - e.g. getting table columns for users without write perms - use 'GET' action when reading table cols * Add info on new user account * Fix boolean expression wrapper * Ruff fixes * Adjust icon * Update unit test * Bummp API version * Table layout fix
This commit is contained in:
@ -1,14 +1,17 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 252
|
||||
INVENTREE_API_VERSION = 253
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v252 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8035
|
||||
v253 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
|
||||
- Adjustments for user API endpoints
|
||||
|
||||
v252 - 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
|
||||
- Add endpoint for listing all known units
|
||||
|
||||
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
||||
|
@ -2,9 +2,13 @@
|
||||
|
||||
import logging
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.metadata import SimpleMetadata
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.utils import model_meta
|
||||
|
||||
import common.models
|
||||
@ -29,6 +33,40 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
so we can perform lookup for ForeignKey related fields.
|
||||
"""
|
||||
|
||||
def determine_actions(self, request, view):
|
||||
"""Determine the 'actions' available to the user for the given view.
|
||||
|
||||
Note that this differs from the standard DRF implementation,
|
||||
in that we also allow annotation for the 'GET' method.
|
||||
|
||||
This allows the client to determine what fields are available,
|
||||
even if they are only for a read (GET) operation.
|
||||
|
||||
See SimpleMetadata.determine_actions for more information.
|
||||
"""
|
||||
actions = {}
|
||||
|
||||
for method in {'PUT', 'POST', 'GET'} & set(view.allowed_methods):
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
if hasattr(view, 'check_permissions'):
|
||||
view.check_permissions(view.request)
|
||||
# Test object permissions
|
||||
if method == 'PUT' and hasattr(view, 'get_object'):
|
||||
view.get_object()
|
||||
except (exceptions.APIException, PermissionDenied, Http404):
|
||||
pass
|
||||
else:
|
||||
# If user has appropriate permissions for the view, include
|
||||
# appropriate metadata about the fields that should be supplied.
|
||||
serializer = view.get_serializer()
|
||||
actions[method] = self.get_serializer_info(serializer)
|
||||
finally:
|
||||
view.request = request
|
||||
|
||||
return actions
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
"""Overwrite the metadata to adapt to the request user."""
|
||||
self.request = request
|
||||
@ -81,6 +119,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
# Map the request method to a permission type
|
||||
rolemap = {
|
||||
'GET': 'view',
|
||||
'POST': 'add',
|
||||
'PUT': 'change',
|
||||
'PATCH': 'change',
|
||||
@ -102,10 +141,6 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
|
||||
actions['DELETE'] = {}
|
||||
|
||||
# Add a 'VIEW' action if we are allowed to view
|
||||
if 'GET' in view.allowed_methods and check(user, table, 'view'):
|
||||
actions['GET'] = {}
|
||||
|
||||
metadata['actions'] = actions
|
||||
|
||||
except AttributeError:
|
||||
|
@ -79,6 +79,9 @@ class RolePermission(permissions.BasePermission):
|
||||
# Extract the model name associated with this request
|
||||
model = get_model_for_view(view)
|
||||
|
||||
if model is None:
|
||||
return True
|
||||
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
|
||||
@ -99,6 +102,17 @@ class IsSuperuser(permissions.IsAdminUser):
|
||||
return bool(request.user and request.user.is_superuser)
|
||||
|
||||
|
||||
class IsSuperuserOrReadOnly(permissions.IsAdminUser):
|
||||
"""Allow read-only access to any user, but write access is restricted to superuser users."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Check if the user is a superuser."""
|
||||
return bool(
|
||||
(request.user and request.user.is_superuser)
|
||||
or request.method in permissions.SAFE_METHODS
|
||||
)
|
||||
|
||||
|
||||
class IsStaffOrReadOnly(permissions.IsAdminUser):
|
||||
"""Allows read-only access to any user, but write access is restricted to staff users."""
|
||||
|
||||
|
@ -403,18 +403,21 @@ class UserSerializer(InvenTreeModelSerializer):
|
||||
read_only_fields = ['username', 'email']
|
||||
|
||||
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
|
||||
|
||||
first_name = serializers.CharField(
|
||||
label=_('First Name'), help_text=_('First name of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
last_name = serializers.CharField(
|
||||
label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
email = serializers.EmailField(
|
||||
label=_('Email'), help_text=_('Email address of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
|
||||
class ExendedUserSerializer(UserSerializer):
|
||||
class ExtendedUserSerializer(UserSerializer):
|
||||
"""Serializer for a User with a bit more info."""
|
||||
|
||||
from users.serializers import GroupSerializer
|
||||
@ -437,9 +440,11 @@ class ExendedUserSerializer(UserSerializer):
|
||||
is_staff = serializers.BooleanField(
|
||||
label=_('Staff'), help_text=_('Does this user have staff permissions')
|
||||
)
|
||||
|
||||
is_superuser = serializers.BooleanField(
|
||||
label=_('Superuser'), help_text=_('Is this user a superuser')
|
||||
)
|
||||
|
||||
is_active = serializers.BooleanField(
|
||||
label=_('Active'), help_text=_('Is this user account active')
|
||||
)
|
||||
@ -464,9 +469,33 @@ class ExendedUserSerializer(UserSerializer):
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class UserCreateSerializer(ExendedUserSerializer):
|
||||
class MeUserSerializer(ExtendedUserSerializer):
|
||||
"""API serializer specifically for the 'me' endpoint."""
|
||||
|
||||
class Meta(ExtendedUserSerializer.Meta):
|
||||
"""Metaclass options.
|
||||
|
||||
Extends the ExtendedUserSerializer.Meta options,
|
||||
but ensures that certain fields are read-only.
|
||||
"""
|
||||
|
||||
read_only_fields = [
|
||||
*ExtendedUserSerializer.Meta.read_only_fields,
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
]
|
||||
|
||||
|
||||
class UserCreateSerializer(ExtendedUserSerializer):
|
||||
"""Serializer for creating a new User."""
|
||||
|
||||
class Meta(ExtendedUserSerializer.Meta):
|
||||
"""Metaclass options for the UserCreateSerializer."""
|
||||
|
||||
# Prevent creation of users with superuser or staff permissions
|
||||
read_only_fields = ['groups', 'is_staff', 'is_superuser']
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Expanded valiadation for auth."""
|
||||
# Check that the user trying to create a new user is a superuser
|
||||
|
@ -24,6 +24,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.permissions
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import (
|
||||
@ -33,7 +34,11 @@ from InvenTree.mixins import (
|
||||
RetrieveUpdateAPI,
|
||||
RetrieveUpdateDestroyAPI,
|
||||
)
|
||||
from InvenTree.serializers import ExendedUserSerializer, UserCreateSerializer
|
||||
from InvenTree.serializers import (
|
||||
ExtendedUserSerializer,
|
||||
MeUserSerializer,
|
||||
UserCreateSerializer,
|
||||
)
|
||||
from InvenTree.settings import FRONTEND_URL_BASE
|
||||
from users.models import ApiToken, Owner
|
||||
from users.serializers import (
|
||||
@ -134,24 +139,38 @@ class UserDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail endpoint for a single user."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = ExendedUserSerializer
|
||||
serializer_class = ExtendedUserSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
||||
"""Detail endpoint for current user."""
|
||||
|
||||
serializer_class = MeUserSerializer
|
||||
|
||||
rolemap = {'POST': 'view', 'PUT': 'view', 'PATCH': 'view'}
|
||||
|
||||
def get_object(self):
|
||||
"""Always return the current user object."""
|
||||
return self.request.user
|
||||
|
||||
def get_permission_model(self):
|
||||
"""Return the model for the permission check.
|
||||
|
||||
Note that for this endpoint, the current user can *always* edit their own details.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
class UserList(ListCreateAPI):
|
||||
"""List endpoint for detail on all users."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserCreateSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
InvenTree.permissions.IsSuperuserOrReadOnly,
|
||||
]
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
search_fields = ['first_name', 'last_name', 'username']
|
||||
|
@ -17,7 +17,10 @@ class UserAPITests(InvenTreeAPITestCase):
|
||||
self.assignRole('admin.add')
|
||||
response = self.options(reverse('api-user-list'), expected_code=200)
|
||||
|
||||
fields = response.data['actions']['POST']
|
||||
# User is *not* a superuser, so user account API is read-only
|
||||
self.assertNotIn('POST', response.data['actions'])
|
||||
|
||||
fields = response.data['actions']['GET']
|
||||
|
||||
# Check some of the field values
|
||||
self.assertEqual(fields['username']['label'], 'Username')
|
||||
|
Reference in New Issue
Block a user