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
« 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
9class SpotSerializer(serializers.ModelSerializer):
10 created_by_username = serializers.CharField(source="created_by.username", read_only=True)
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"]
17 def get_created_by(self, obj):
18 return obj.created_by.username if hasattr(obj, "created_by") and obj.created_by else None
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)
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
39class ParkingLotSerializer(serializers.ModelSerializer):
40 class Meta:
41 model = ParkingLot
42 fields = ['id', 'name', 'city', 'street', 'building']
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']
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
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()
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()
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
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"]
78class OperatorBookingCancelSerializer(serializers.Serializer):
79 reason = serializers.CharField(max_length=255, required=True,
80 help_text="Причина скасування бронювання оператором.")
82class BookingCreateSerializer(serializers.ModelSerializer):
83 class Meta:
84 model = Booking
85 fields = ["spot", "start_at", "end_at"]
87class BookingCancelSerializer(serializers.Serializer):
88 reason = serializers.CharField(required=False, allow_blank=True, max_length=200)
90class UserRegistrationSerializer(serializers.ModelSerializer):
92 password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
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 }
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
113class UserRegistrationSerializer(serializers.ModelSerializer):
114 password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
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 }
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
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')
144 def get_is_operator(self, obj):
145 return hasattr(obj, 'operator_profile')
147 def get_lot_id(self, obj):
148 if hasattr(obj, 'operator_profile'):
149 return obj.operator_profile.lot_id
150 return None
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 }
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"]
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)
172class OperatorAssignSerializer(serializers.Serializer):
173 lot_id = serializers.IntegerField(required=True)
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