Coverage for src/api/serializers.py: 94%

115 statements  

« prev     ^ index     » next       coverage.py v7.11.1, created at 2025-11-08 10:41 +0000

1from rest_framework import serializers 

2from .models import ParkingLot, Spot, Booking, OperatorProfile 

3from django.contrib.auth.models import User 

4from django.contrib.auth.password_validation import validate_password 

5import re 

6from rest_framework.validators import UniqueTogetherValidator 

7 

8 

9class SpotSerializer(serializers.ModelSerializer): 

10 created_by_username = serializers.CharField(source="created_by.username", read_only=True) 

11 

12 class Meta: 

13 model = Spot 

14 fields = ["id", "number", "is_ev", "is_disabled", "lot", "created_by", "created_by_username"] 

15 read_only_fields = ["lot", "created_by"] 

16 

17 def get_created_by(self, obj): 

18 return obj.created_by.username if hasattr(obj, "created_by") and obj.created_by else None 

19 

20 def validate(self, attrs): 

21 """Ensure unique spot number within the same lot (case-insensitive).""" 

22 lot = self.instance.lot if self.instance else self.context.get("lot") 

23 number = attrs.get("number") or (self.instance.number if self.instance else None) 

24 

25 if lot and number: 

26 qs = Spot.objects.filter(lot=lot, number__iexact=number) 

27 if self.instance: 

28 qs = qs.exclude(pk=self.instance.pk) 

29 if qs.exists(): 

30 existing_numbers = Spot.objects.filter(lot=lot).values_list("number", flat=True) 

31 raise serializers.ValidationError( 

32 { 

33 "number": f"This spot number already exists in lot '{lot.name}' (case-insensitive check).", 

34 "existing_numbers": list(existing_numbers), 

35 } 

36 ) 

37 return attrs 

38 

39class ParkingLotSerializer(serializers.ModelSerializer): 

40 class Meta: 

41 model = ParkingLot 

42 fields = ['id', 'name', 'city', 'street', 'building'] 

43 

44class ParkingLotDetailSerializer(ParkingLotSerializer): 

45 spots = SpotSerializer(many=True, read_only=True) 

46 class Meta: 

47 model = ParkingLot 

48 fields = ['id', 'name', 'city', 'street', 'building', 'spots'] 

49 

50 def validate_name(self, value): 

51 if len(value.strip()) < 3: 

52 raise serializers.ValidationError("Name must contain at least 3 characters.") 

53 return value 

54 

55 def validate_city(self, value): 

56 cleaned_value = value.replace('-', '').replace(' ', '') 

57 if not cleaned_value.isalpha(): 

58 raise serializers.ValidationError("City name must contain only letters, spaces, or hyphens.") 

59 return value.title() 

60 

61 def validate_street(self, value): 

62 if len(value.strip()) < 3: 

63 raise serializers.ValidationError("Street name must contain at least 3 characters.") 

64 return value.title() 

65 

66 def validate_building(self, value): 

67 if value and not re.match(r'^\d+[A-Za-z\-]*$', value): 

68 raise serializers.ValidationError("Invalid building number format.") 

69 return value 

70 

71 

72class BookingSerializer(serializers.ModelSerializer): 

73 class Meta: 

74 model = Booking 

75 fields = ["id", "user", "spot", "start_at", "end_at", "status", "created_at", "cancellation_reason", "payment_intent_id"] 

76 read_only_fields = ["status", "created_at", "user", "cancellation_reason", "payment_intent_id"] 

77 

78class OperatorBookingCancelSerializer(serializers.Serializer): 

79 reason = serializers.CharField(max_length=255, required=True, 

80 help_text="Причина скасування бронювання оператором.") 

81 

82class BookingCreateSerializer(serializers.ModelSerializer): 

83 class Meta: 

84 model = Booking 

85 fields = ["spot", "start_at", "end_at"] 

86 

87class BookingCancelSerializer(serializers.Serializer): 

88 reason = serializers.CharField(required=False, allow_blank=True, max_length=200) 

89 

90class UserRegistrationSerializer(serializers.ModelSerializer): 

91 

92 password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) 

93 

94 class Meta: 

95 model = User 

96 fields = ('username', 'email', 'password', 'first_name', 'last_name') 

97 extra_kwargs = { 

98 'first_name': {'required': False}, 

99 'last_name': {'required': False}, 

100 'email': {'required': True}, 

101 } 

102 

103 def create(self, validated_data): 

104 user = User.objects.create_user( 

105 username=validated_data['username'], 

106 email=validated_data['email'], 

107 password=validated_data['password'], 

108 first_name=validated_data.get('first_name', ''), 

109 last_name=validated_data.get('last_name', '') 

110 ) 

111 return user 

112 

113class UserRegistrationSerializer(serializers.ModelSerializer): 

114 password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) 

115 

116 class Meta: 

117 model = User 

118 fields = ('username', 'email', 'password', 'first_name', 'last_name') 

119 extra_kwargs = { 

120 'first_name': {'required': False}, 

121 'last_name': {'required': False}, 

122 'email': {'required': True}, 

123 } 

124 

125 def create(self, validated_data): 

126 user = User.objects.create_user( 

127 username=validated_data['username'], 

128 email=validated_data['email'], 

129 password=validated_data['password'], 

130 first_name=validated_data.get('first_name', ''), 

131 last_name=validated_data.get('last_name', '') 

132 ) 

133 return user 

134 

135class UserSerializer(serializers.ModelSerializer): 

136 is_operator = serializers.SerializerMethodField() 

137 lot_id = serializers.SerializerMethodField() 

138 is_staff = serializers.BooleanField() 

139 class Meta: 

140 model = User 

141 fields = ('id', 'username', 'email', 'first_name', 'last_name', 'is_operator', 'lot_id', 'is_staff') 

142 read_only_fields = ('id', 'username', 'email', 'is_operator', 'lot_id', 'is_staff') 

143 

144 def get_is_operator(self, obj): 

145 return hasattr(obj, 'operator_profile') 

146 

147 def get_lot_id(self, obj): 

148 if hasattr(obj, 'operator_profile'): 

149 return obj.operator_profile.lot_id 

150 return None 

151 

152class UserProfileUpdateSerializer(serializers.ModelSerializer): 

153 class Meta: 

154 model = User 

155 fields = ('first_name', 'last_name') 

156 extra_kwargs = { 

157 'first_name': {'required': False}, 

158 'last_name': {'required': False}, 

159 } 

160 

161class SpotOperatorUpdateSerializer(serializers.ModelSerializer): 

162 """Serializer for operators — restricts updates to allowed fields.""" 

163 class Meta: 

164 model = Spot 

165 fields = ["is_ev", "is_disabled"] 

166 

167 def validate(self, attrs): 

168 if "number" in self.initial_data: 

169 raise serializers.ValidationError({"number": "Operators cannot change the spot number."}) 

170 return super().validate(attrs) 

171 

172class OperatorAssignSerializer(serializers.Serializer): 

173 lot_id = serializers.IntegerField(required=True) 

174 

175 def validate_lot_id(self, value): 

176 if not ParkingLot.objects.filter(pk=value).exists(): 

177 raise serializers.ValidationError("Parking lot with this ID does not exist.") 

178 return value