petcare-api / api /serializers.py
Sameer669
Initial PawCare Django backend with JWT auth, RBAC, audit logging, and HF storage
4f01198
"""
DRF Serializers β€” validation, representation, and nested relations.
"""
from django.contrib.auth import get_user_model
from django.utils import timezone
from rest_framework import serializers
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from api.models import (
Caregiver, CaregiverService, Pet, Service,
Booking, BookingStatus, Conversation, Message, Role,
)
User = get_user_model()
# ─────────────────────────────────────────────────────────────────────────────
# JWT Custom Claims
# ─────────────────────────────────────────────────────────────────────────────
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token['email'] = user.email
token['role'] = user.role
token['full_name'] = user.full_name
return token
def validate(self, attrs):
data = super().validate(attrs)
data['user'] = {
'id': str(self.user.id),
'email': self.user.email,
'role': self.user.role,
'full_name': self.user.full_name,
'avatar_url': self.user.avatar_url,
}
return data
# ─────────────────────────────────────────────────────────────────────────────
# Auth Serializers
# ─────────────────────────────────────────────────────────────────────────────
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=10)
password_confirm = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['email', 'first_name', 'last_name', 'password', 'password_confirm']
def validate(self, data):
if data['password'] != data['password_confirm']:
raise serializers.ValidationError({'password_confirm': 'Passwords do not match.'})
return data
def create(self, validated_data):
validated_data.pop('password_confirm')
return User.objects.create_user(
role=Role.USER,
**validated_data,
)
class UserProfileSerializer(serializers.ModelSerializer):
full_name = serializers.ReadOnlyField()
class Meta:
model = User
fields = [
'id', 'email', 'first_name', 'last_name', 'full_name',
'role', 'phone', 'address', 'avatar_url', 'date_joined',
]
read_only_fields = ['id', 'role', 'date_joined']
class UserPublicSerializer(serializers.ModelSerializer):
"""Minimal public view β€” no PII."""
full_name = serializers.ReadOnlyField()
class Meta:
model = User
fields = ['id', 'full_name', 'avatar_url']
# ─────────────────────────────────────────────────────────────────────────────
# Service
# ─────────────────────────────────────────────────────────────────────────────
class ServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Service
fields = '__all__'
# ─────────────────────────────────────────────────────────────────────────────
# Caregiver
# ─────────────────────────────────────────────────────────────────────────────
class CaregiverServiceSerializer(serializers.ModelSerializer):
service_name = serializers.ReadOnlyField(source='service.name')
service_icon = serializers.ReadOnlyField(source='service.icon')
class Meta:
model = CaregiverService
fields = ['service', 'service_name', 'service_icon', 'price_per_hour', 'is_active']
class CaregiverListSerializer(serializers.ModelSerializer):
"""Lightweight serializer for list views / search."""
full_name = serializers.ReadOnlyField(source='user.full_name')
avatar_url = serializers.ReadOnlyField(source='user.avatar_url')
class Meta:
model = Caregiver
fields = [
'id', 'full_name', 'avatar_url', 'profile_image_url',
'city', 'country', 'rating', 'total_reviews', 'total_bookings',
'years_of_experience', 'specializations', 'is_available',
'is_verified', 'is_featured',
]
class CaregiverDetailSerializer(serializers.ModelSerializer):
"""Full detail including services and user profile."""
full_name = serializers.ReadOnlyField(source='user.full_name')
email = serializers.ReadOnlyField(source='user.email')
avatar_url = serializers.ReadOnlyField(source='user.avatar_url')
caregiver_services = CaregiverServiceSerializer(
source='caregiverservice_set', many=True, read_only=True
)
class Meta:
model = Caregiver
fields = [
'id', 'full_name', 'email', 'avatar_url',
'profile_image_url', 'gallery_images', 'bio',
'years_of_experience', 'specializations', 'certifications', 'languages',
'city', 'country', 'latitude', 'longitude',
'rating', 'total_reviews', 'total_bookings',
'is_available', 'is_verified', 'is_featured', 'background_check_passed',
'caregiver_services', 'created_at',
]
read_only_fields = ['rating', 'total_reviews', 'total_bookings', 'created_at']
class CaregiverWriteSerializer(serializers.ModelSerializer):
"""Admin-only β€” create/update caregiver profiles."""
email = serializers.EmailField(write_only=True)
first_name = serializers.CharField(write_only=True, max_length=100)
last_name = serializers.CharField(write_only=True, max_length=100)
password = serializers.CharField(write_only=True, min_length=10, required=False)
class Meta:
model = Caregiver
fields = [
'email', 'first_name', 'last_name', 'password',
'bio', 'years_of_experience', 'specializations', 'certifications',
'languages', 'city', 'country', 'latitude', 'longitude',
'is_available', 'is_verified', 'is_featured', 'background_check_passed',
'emergency_contact',
]
def create(self, validated_data):
email = validated_data.pop('email')
first_name = validated_data.pop('first_name')
last_name = validated_data.pop('last_name')
password = validated_data.pop('password', User.objects.make_random_password(16))
user = User.objects.create_user(
email=email,
first_name=first_name,
last_name=last_name,
password=password,
role=Role.CAREGIVER,
)
return Caregiver.objects.create(user=user, **validated_data)
def update(self, instance, validated_data):
# Update nested user fields if provided
for field in ('email', 'first_name', 'last_name'):
val = validated_data.pop(field, None)
if val:
setattr(instance.user, field, val)
password = validated_data.pop('password', None)
if password:
instance.user.set_password(password)
instance.user.save()
for attr, val in validated_data.items():
setattr(instance, attr, val)
instance.save()
return instance
# ─────────────────────────────────────────────────────────────────────────────
# Pet
# ─────────────────────────────────────────────────────────────────────────────
class PetSerializer(serializers.ModelSerializer):
class Meta:
model = Pet
fields = '__all__'
read_only_fields = ['id', 'owner', 'created_at', 'updated_at']
# ─────────────────────────────────────────────────────────────────────────────
# Booking
# ─────────────────────────────────────────────────────────────────────────────
class BookingCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Booking
fields = [
'caregiver', 'pet', 'service',
'scheduled_start', 'scheduled_end',
'notes', 'service_address',
]
def validate(self, data):
if data['scheduled_end'] <= data['scheduled_start']:
raise serializers.ValidationError('End time must be after start time.')
# Ensure pet belongs to the requesting user
request = self.context['request']
if data['pet'].owner != request.user:
raise serializers.ValidationError({'pet': 'You can only book for your own pets.'})
return data
def create(self, validated_data):
user = self.context['request'].user
service = validated_data['service']
caregiver = validated_data['caregiver']
# Calculate pricing
hours = (
validated_data['scheduled_end'] - validated_data['scheduled_start']
).total_seconds() / 3600
try:
cs = CaregiverService.objects.get(
caregiver=caregiver, service=service, is_active=True
)
subtotal = cs.price_per_hour * hours
except CaregiverService.DoesNotExist:
subtotal = service.base_price * hours
fees = subtotal * 0.05 # 5% platform fee
total = subtotal + fees
return Booking.objects.create(
user=user,
price_subtotal=round(subtotal, 2),
price_fees=round(fees, 2),
price_total=round(total, 2),
**validated_data,
)
class BookingSerializer(serializers.ModelSerializer):
user = UserPublicSerializer(read_only=True)
caregiver = CaregiverListSerializer(read_only=True)
service = ServiceSerializer(read_only=True)
pet = PetSerializer(read_only=True)
class Meta:
model = Booking
fields = '__all__'
read_only_fields = [
'id', 'user', 'price_subtotal', 'price_fees', 'price_total',
'created_at', 'updated_at',
]
class BookingStatusUpdateSerializer(serializers.Serializer):
status = serializers.ChoiceField(choices=BookingStatus.choices)
cancellation_reason = serializers.CharField(required=False, allow_blank=True)
class BookingReviewSerializer(serializers.Serializer):
rating = serializers.IntegerField(min_value=1, max_value=5)
review = serializers.CharField(max_length=1000, allow_blank=True)
def update(self, instance, validated_data):
instance.rating = validated_data['rating']
instance.review = validated_data.get('review', '')
instance.reviewed_at = timezone.now()
instance.save()
# Update caregiver aggregate rating
caregiver = instance.caregiver
bookings_with_reviews = Booking.objects.filter(
caregiver=caregiver, rating__isnull=False
)
count = bookings_with_reviews.count()
if count:
avg = sum(b.rating for b in bookings_with_reviews) / count
caregiver.rating = round(avg, 2)
caregiver.total_reviews = count
caregiver.save(update_fields=['rating', 'total_reviews'])
return instance
# ─────────────────────────────────────────────────────────────────────────────
# Messaging
# ─────────────────────────────────────────────────────────────────────────────
class MessageSerializer(serializers.ModelSerializer):
sender = UserPublicSerializer(read_only=True)
class Meta:
model = Message
fields = [
'id', 'conversation', 'sender', 'message_type',
'content', 'image_url', 'is_read', 'read_at', 'created_at',
]
read_only_fields = ['id', 'sender', 'is_read', 'read_at', 'created_at']
class ConversationSerializer(serializers.ModelSerializer):
participants = UserPublicSerializer(many=True, read_only=True)
last_message = serializers.SerializerMethodField()
unread_count = serializers.SerializerMethodField()
class Meta:
model = Conversation
fields = [
'id', 'participants', 'booking', 'last_message',
'unread_count', 'created_at', 'updated_at',
]
def get_last_message(self, obj):
msg = obj.messages.last()
if msg:
return {'content': msg.content[:80], 'created_at': msg.created_at}
return None
def get_unread_count(self, obj):
user = self.context['request'].user
return obj.messages.filter(is_read=False).exclude(sender=user).count()
# ─────────────────────────────────────────────────────────────────────────────
# Image Upload
# ─────────────────────────────────────────────────────────────────────────────
class ImageUploadSerializer(serializers.Serializer):
image = serializers.ImageField()
folder = serializers.ChoiceField(
choices=['caregivers', 'pets', 'messages'],
default='caregivers',
)