Part 1: Booking API with Swagger and custom endpoints

This commit is contained in:
2025-09-30 23:09:47 +03:00
commit e6dc52ca4d
33 changed files with 573 additions and 0 deletions
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
View File
@@ -0,0 +1,11 @@
from django.contrib import admin
from .models import Venue, Booking
@admin.register(Venue)
class VenueAdmin(admin.ModelAdmin):
list_display = ("id", "name", "capacity", "address")
@admin.register(Booking)
class BookingAdmin(admin.ModelAdmin):
list_display = ("id", "venue", "customer_name", "start_time", "end_time", "status")
list_filter = ("status", "venue")
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BookingsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'bookings'
+41
View File
@@ -0,0 +1,41 @@
# bookings/middleware.py
import logging
import time
import json
logger = logging.getLogger(__name__)
class RequestLoggingMiddleware:
"""
Logs request start, method, path, body (if small), response status and duration.
Place this middleware after common Django middleware as configured in settings.py.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.time()
try:
# basic request info
method = request.method
path = request.get_full_path()
# try to capture small JSON bodies safely
body = None
try:
if method in ("POST", "PUT", "PATCH") and request.body:
raw = request.body.decode("utf-8")[:2000]
body = raw
except Exception:
body = "<could not decode>"
logger.info(f"Request start: {method} {path} body={body}")
response = self.get_response(request)
duration = time.time() - start
logger.info(f"Request finished: {method} {path} status={response.status_code} time={duration:.3f}s")
return response
except Exception as ex:
# log exception details
logger.exception(f"Unhandled exception during request: {ex}")
raise
+37
View File
@@ -0,0 +1,37 @@
# Generated by Django 5.2.6 on 2025-09-30 18:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Venue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('address', models.CharField(blank=True, max_length=300)),
('capacity', models.PositiveIntegerField(default=0)),
],
),
migrations.CreateModel(
name='Booking',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('customer_name', models.CharField(max_length=200)),
('customer_email', models.EmailField(max_length=254)),
('start_time', models.DateTimeField()),
('end_time', models.DateTimeField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('status', models.CharField(choices=[('CONFIRMED', 'Confirmed'), ('CANCELLED', 'Cancelled')], default='CONFIRMED', max_length=20)),
('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to='bookings.venue')),
],
),
]
View File
+32
View File
@@ -0,0 +1,32 @@
from django.db import models
# Create your models here.# bookings/models.py
class Venue(models.Model):
name = models.CharField(max_length=100)
city = models.CharField(max_length=50)
capacity = models.IntegerField()
description = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to="venues/", blank=True, null=True)
def __str__(self):
return f"{self.name} ({self.city})"
class Booking(models.Model):
venue = models.ForeignKey(Venue, related_name="bookings", on_delete=models.CASCADE)
customer_name = models.CharField(max_length=200)
customer_email = models.EmailField()
start_time = models.DateTimeField()
end_time = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
STATUS_CHOICES = [
("CONFIRMED", "Confirmed"),
("CANCELLED", "Cancelled"),
]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="CONFIRMED")
def __str__(self):
return f"Booking {self.id} at {self.venue.name} for {self.customer_name}"
+15
View File
@@ -0,0 +1,15 @@
# bookings/serializers.py
from rest_framework import serializers
from .models import Venue, Booking
class VenueSerializer(serializers.ModelSerializer):
class Meta:
model = Venue
fields = "__all__"
class BookingSerializer(serializers.ModelSerializer):
venue = serializers.PrimaryKeyRelatedField(queryset=Venue.objects.all())
class Meta:
model = Booking
fields = ["id", "venue", "customer_name", "customer_email", "start_time", "end_time", "status", "created_at"]
read_only_fields = ["created_at", "status"]
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+9
View File
@@ -0,0 +1,9 @@
# bookings/urls.py
from rest_framework import routers
from .views import VenueViewSet, BookingViewSet
router = routers.DefaultRouter()
router.register(r"venues", VenueViewSet, basename="venues")
router.register(r"bookings", BookingViewSet, basename="bookings")
urlpatterns = router.urls
+111
View File
@@ -0,0 +1,111 @@
from django.shortcuts import render
# Create your views here.
# bookings/views.py
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Venue, Booking
from .serializers import VenueSerializer, BookingSerializer
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework.decorators import action
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import Venue, Booking
from .serializers import VenueSerializer, BookingSerializer
class VenueViewSet(viewsets.ModelViewSet):
queryset = Venue.objects.all()
serializer_class = VenueSerializer
# Custom endpoint: /venues/findByCity?city=Paris
@action(detail=False, methods=["get"])
def findByCity(self, request):
city = request.query_params.get("city")
venues = Venue.objects.filter(city__iexact=city) if city else Venue.objects.all()
serializer = self.get_serializer(venues, many=True)
return Response(serializer.data)
# Custom endpoint: /venues/{id}/availability
@action(detail=True, methods=["get"])
def availability(self, request, pk=None):
venue = self.get_object()
bookings = Booking.objects.filter(venue=venue, status="active")
return Response({
"venue": venue.name,
"city": venue.city,
"capacity": venue.capacity,
"active_bookings": bookings.count(),
"available": venue.capacity - bookings.count()
})
# Custom endpoint: /venues/{id}/uploadImage
@action(detail=True, methods=["post"])
def uploadImage(self, request, pk=None):
venue = self.get_object()
file = request.FILES.get("file")
if not file:
return Response({"error": "No file uploaded"}, status=400)
venue.image = file
venue.save()
return Response({"status": "Image uploaded", "venue": venue.name})
# Custom endpoint: /venues/{id}/bookings
@action(detail=True, methods=["get"])
def bookings(self, request, pk=None):
venue = self.get_object()
bookings = Booking.objects.filter(venue=venue)
serializer = BookingSerializer(bookings, many=True)
return Response(serializer.data)
class BookingViewSet(viewsets.ModelViewSet):
queryset = Booking.objects.all().order_by("-created_at")
serializer_class = BookingSerializer
filter_backends = [filters.SearchFilter]
search_fields = ["customer_name", "customer_email"]
def get_queryset(self):
qs = super().get_queryset()
venue_id = self.request.query_params.get("venue")
if venue_id:
qs = qs.filter(venue_id=venue_id)
return qs
# Example custom action: cancel booking (POST /api/bookings/{id}/cancel/)
@action(detail=True, methods=["post"])
def cancel(self, request, pk=None):
booking = self.get_object()
booking.status = "cancelled"
booking.save()
return Response(
{"status": "Booking cancelled", "id": booking.id},
status=status.HTTP_200_OK
)
# Custom endpoint: /bookings/findByStatus?status=active
@action(detail=False, methods=["get"])
def findByStatus(self, request):
status_param = request.query_params.get("status", None)
if status_param:
bookings = Booking.objects.filter(status=status_param)
else:
bookings = Booking.objects.all()
serializer = self.get_serializer(bookings, many=True)
return Response(serializer.data)
# Custom endpoint: /bookings/{id}/uploadDocument
@action(detail=True, methods=["post"])
def uploadDocument(self, request, pk=None):
booking = self.get_object()
file = request.FILES.get("file")
if not file:
return Response({"error": "No file uploaded"}, status=400)
# In a real app, save file to booking record or storage
return Response(
{"status": "File uploaded", "filename": file.name, "booking_id": booking.id},
status=200
)