Coverage for users/api/serializers_/users.py: 74%

44 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from django.contrib.auth import password_validation 

2from drf_spectacular.types import OpenApiTypes 

3from drf_spectacular.utils import extend_schema_field 

4from rest_framework import serializers 

5 

6from netbox.api.fields import SerializedPKRelatedField 

7from netbox.api.serializers import ValidatedModelSerializer 

8from users.models import Group, ObjectPermission, User 

9 

10from .permissions import ObjectPermissionSerializer 

11 

12__all__ = ( 

13 'GroupSerializer', 

14 'UserSerializer', 

15) 

16 

17 

18class GroupSerializer(ValidatedModelSerializer): 

19 user_count = serializers.IntegerField(read_only=True) 

20 permissions = SerializedPKRelatedField( 

21 source='object_permissions', 

22 queryset=ObjectPermission.objects.all(), 

23 serializer=ObjectPermissionSerializer, 

24 nested=True, 

25 required=False, 

26 many=True 

27 ) 

28 

29 class Meta: 

30 model = Group 

31 fields = ('id', 'url', 'display_url', 'display', 'name', 'description', 'permissions', 'user_count') 

32 brief_fields = ('id', 'url', 'display', 'name', 'description') 

33 

34 

35class UserSerializer(ValidatedModelSerializer): 

36 groups = SerializedPKRelatedField( 

37 queryset=Group.objects.all(), 

38 serializer=GroupSerializer, 

39 nested=True, 

40 required=False, 

41 many=True 

42 ) 

43 permissions = SerializedPKRelatedField( 

44 source='object_permissions', 

45 queryset=ObjectPermission.objects.all(), 

46 serializer=ObjectPermissionSerializer, 

47 nested=True, 

48 required=False, 

49 many=True 

50 ) 

51 

52 class Meta: 

53 model = User 

54 fields = ( 

55 'id', 'url', 'display_url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 

56 'is_active', 'date_joined', 'last_login', 'groups', 'permissions', 

57 ) 

58 brief_fields = ('id', 'url', 'display', 'username') 

59 extra_kwargs = { 

60 'password': {'write_only': True} 

61 } 

62 

63 def validate(self, data): 

64 

65 # Enforce password validation rules (if configured) 

66 if not self.nested and data.get('password'): 

67 password_validation.validate_password(data['password'], self.instance) 

68 

69 return super().validate(data) 

70 

71 def create(self, validated_data): 

72 """ 

73 Extract the password from validated data and set it separately to ensure proper hash generation. 

74 """ 

75 password = validated_data.pop('password') 

76 user = super().create(validated_data) 

77 user.set_password(password) 

78 user.save() 

79 

80 return user 

81 

82 def update(self, instance, validated_data): 

83 """ 

84 Ensure proper updated password hash generation. 

85 """ 

86 password = validated_data.pop('password', None) 

87 if password is not None: 

88 instance.set_password(password) 

89 

90 return super().update(instance, validated_data) 

91 

92 @extend_schema_field(OpenApiTypes.STR) 

93 def get_display(self, obj): 

94 if full_name := obj.get_full_name(): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true

95 return f"{obj.username} ({full_name})" 

96 return obj.username