mirror of
https://github.com/Cesarkassis/booking-Api.git
synced 2026-10-19 18:56:17 +00:00
Part 1: Booking API with Swagger and custom endpoints
This commit is contained in:
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.
@@ -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")
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BookingsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'bookings'
|
||||
@@ -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
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@@ -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}"
|
||||
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user