feat: add API documentation with drf-spectacular and refactor views
- Install and configure drf-spectacular for OpenAPI/Swagger documentation - Add Swagger UI endpoints at /api/schema/ and /api/docs/ - Configure SPECTACULAR_SETTINGS with API metadata - Refactor meetings views from function-based to class-based views (ScheduleAppointmentView, RejectAppointmentView, AvailableDatesView, UserAppointmentsView, AppointmentStatsView, UserAppointmentStatsView) - Update URL patterns to use new class-based views - Simplify ALLOWED_HOSTS configuration to accept all hosts This improves API discoverability through interactive documentation and modernizes the codebase by using class-based views for better code organization and reusability.
This commit is contained in:
parent
9aef796fc7
commit
1ffbfa5692
@ -12,7 +12,7 @@ SECRET_KEY = os.getenv('JWT_SECRET', 'django-insecure-fallback-secret-key')
|
|||||||
|
|
||||||
DEBUG = os.getenv('DEBUG')
|
DEBUG = os.getenv('DEBUG')
|
||||||
|
|
||||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '*').split(',')
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS = os.getenv(
|
CORS_ALLOWED_ORIGINS = os.getenv(
|
||||||
'CORS_ALLOWED_ORIGINS',
|
'CORS_ALLOWED_ORIGINS',
|
||||||
@ -33,6 +33,7 @@ INSTALLED_APPS = [
|
|||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework_simplejwt',
|
'rest_framework_simplejwt',
|
||||||
'corsheaders',
|
'corsheaders',
|
||||||
|
'drf_spectacular',
|
||||||
|
|
||||||
'users',
|
'users',
|
||||||
'meetings',
|
'meetings',
|
||||||
@ -142,6 +143,7 @@ DEFAULT_FROM_EMAIL = os.getenv('SMTP_FROM', 'hello@attunehearttherapy.com')
|
|||||||
|
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
),
|
),
|
||||||
@ -153,6 +155,14 @@ REST_FRAMEWORK = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Configure Spectacular settings
|
||||||
|
SPECTACULAR_SETTINGS = {
|
||||||
|
'TITLE': 'Blog API',
|
||||||
|
'DESCRIPTION': 'API for managing users, meetings, and more.',
|
||||||
|
'VERSION': '1.0.0',
|
||||||
|
'SERVE_INCLUDE_SCHEMA': False,
|
||||||
|
}
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS = [
|
CORS_ALLOWED_ORIGINS = [
|
||||||
"http://localhost:3000",
|
"http://localhost:3000",
|
||||||
"http://127.0.0.1:3000",
|
"http://127.0.0.1:3000",
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .views import api_root
|
from .views import api_root
|
||||||
|
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('api/auth/', include('users.urls')),
|
path('api/auth/', include('users.urls')),
|
||||||
path('api/meetings/', include('meetings.urls')),
|
path('api/meetings/', include('meetings.urls')),
|
||||||
path('', api_root, name='api-root'),
|
path('', api_root, name='api-root'),
|
||||||
|
|
||||||
|
# Swagger UI endpoints
|
||||||
|
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
|
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
]
|
]
|
||||||
@ -4,12 +4,12 @@ from .views import (
|
|||||||
AppointmentRequestListView,
|
AppointmentRequestListView,
|
||||||
AppointmentRequestCreateView,
|
AppointmentRequestCreateView,
|
||||||
AppointmentRequestDetailView,
|
AppointmentRequestDetailView,
|
||||||
schedule_appointment,
|
ScheduleAppointmentView,
|
||||||
reject_appointment,
|
RejectAppointmentView,
|
||||||
available_dates,
|
AvailableDatesView,
|
||||||
user_appointments,
|
UserAppointmentsView,
|
||||||
appointment_stats,
|
AppointmentStatsView,
|
||||||
user_apointment_stats
|
UserAppointmentStatsView
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -19,12 +19,12 @@ urlpatterns = [
|
|||||||
path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'),
|
path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'),
|
||||||
path('appointments/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'),
|
path('appointments/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'),
|
||||||
|
|
||||||
path('appointments/<uuid:pk>/schedule/', schedule_appointment, name='appointment-schedule'),
|
path('appointments/<uuid:pk>/schedule/', ScheduleAppointmentView.as_view(), name='appointment-schedule'),
|
||||||
path('appointments/<uuid:pk>/reject/', reject_appointment, name='appointment-reject'),
|
path('appointments/<uuid:pk>/reject/', RejectAppointmentView.as_view(), name='appointment-reject'),
|
||||||
|
|
||||||
path('appointments/available-dates/', available_dates, name='available-dates'),
|
path('appointments/available-dates/', AvailableDatesView.as_view(), name='available-dates'),
|
||||||
path('user/appointments/', user_appointments, name='user-appointments'),
|
path('user/appointments/', UserAppointmentsView.as_view(), name='user-appointments'),
|
||||||
|
|
||||||
path('appointments/stats/', appointment_stats, name='appointment-stats'),
|
path('appointments/stats/', AppointmentStatsView.as_view(), name='appointment-stats'),
|
||||||
path('user/appointments/stats/', user_apointment_stats, name='user-appointment-stats'),
|
path('user/appointments/stats/', UserAppointmentStatsView.as_view(), name='user-appointment-stats'),
|
||||||
]
|
]
|
||||||
@ -1,7 +1,7 @@
|
|||||||
from rest_framework import generics, status
|
from rest_framework import generics, status
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
from rest_framework.permissions import IsAuthenticated, AllowAny,IsAdminUser
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from .models import AdminWeeklyAvailability, AppointmentRequest
|
from .models import AdminWeeklyAvailability, AppointmentRequest
|
||||||
@ -14,9 +14,11 @@ from .serializers import (
|
|||||||
)
|
)
|
||||||
from .email_service import EmailService
|
from .email_service import EmailService
|
||||||
from users.models import CustomUser
|
from users.models import CustomUser
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
|
|
||||||
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
|
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||||
serializer_class = AdminWeeklyAvailabilitySerializer
|
serializer_class = AdminWeeklyAvailabilitySerializer
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
@ -63,22 +65,24 @@ class AppointmentRequestDetailView(generics.RetrieveAPIView):
|
|||||||
serializer_class = AppointmentRequestSerializer
|
serializer_class = AppointmentRequestSerializer
|
||||||
lookup_field = 'pk'
|
lookup_field = 'pk'
|
||||||
|
|
||||||
@api_view(['POST'])
|
class ScheduleAppointmentView(generics.GenericAPIView):
|
||||||
@permission_classes([IsAuthenticated])
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||||
def schedule_appointment(request, pk):
|
serializer_class = AppointmentScheduleSerializer
|
||||||
try:
|
queryset = AppointmentRequest.objects.all()
|
||||||
appointment = AppointmentRequest.objects.get(pk=pk)
|
lookup_field = 'pk'
|
||||||
except AppointmentRequest.DoesNotExist:
|
|
||||||
return Response({'error': 'Appointment not found'}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
if appointment.status != 'pending_review':
|
def post(self, request, pk):
|
||||||
return Response(
|
appointment = self.get_object()
|
||||||
{'error': 'Only pending review appointments can be scheduled.'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
if appointment.status != 'pending_review':
|
||||||
)
|
return Response(
|
||||||
|
{'error': 'Only pending review appointments can be scheduled.'},
|
||||||
serializer = AppointmentScheduleSerializer(data=request.data)
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
if serializer.is_valid():
|
)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
appointment.schedule_appointment(
|
appointment.schedule_appointment(
|
||||||
datetime_obj=serializer.validated_data['scheduled_datetime'],
|
datetime_obj=serializer.validated_data['scheduled_datetime'],
|
||||||
duration=serializer.validated_data['scheduled_duration']
|
duration=serializer.validated_data['scheduled_duration']
|
||||||
@ -92,104 +96,106 @@ def schedule_appointment(request, pk):
|
|||||||
'message': 'Appointment scheduled successfully. Jitsi meeting created.',
|
'message': 'Appointment scheduled successfully. Jitsi meeting created.',
|
||||||
'jitsi_meeting_created': appointment.has_jitsi_meeting
|
'jitsi_meeting_created': appointment.has_jitsi_meeting
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
class RejectAppointmentView(generics.GenericAPIView):
|
||||||
@permission_classes([IsAuthenticated])
|
permission_classes = [IsAuthenticated]
|
||||||
def reject_appointment(request, pk):
|
serializer_class = AppointmentRejectSerializer
|
||||||
try:
|
queryset = AppointmentRequest.objects.all()
|
||||||
appointment = AppointmentRequest.objects.get(pk=pk)
|
lookup_field = 'pk'
|
||||||
except AppointmentRequest.DoesNotExist:
|
|
||||||
return Response({'error': 'Appointment not found'}, status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
if appointment.status != 'pending_review':
|
def post(self, request, pk):
|
||||||
return Response(
|
appointment = self.get_object()
|
||||||
{'error': 'Only pending appointments can be rejected'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
if appointment.status != 'pending_review':
|
||||||
|
return Response(
|
||||||
|
{'error': 'Only pending appointments can be rejected'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
appointment.reject_appointment(
|
||||||
|
serializer.validated_data.get('rejection_reason', '')
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = AppointmentRejectSerializer(data=request.data)
|
|
||||||
if serializer.is_valid():
|
|
||||||
appointment.reject_appointment(serializer.validated_data.get('rejection_reason', ''))
|
|
||||||
EmailService.send_appointment_rejected(appointment)
|
EmailService.send_appointment_rejected(appointment)
|
||||||
return Response(AppointmentRequestSerializer(appointment).data)
|
|
||||||
|
response_serializer = AppointmentRequestSerializer(appointment)
|
||||||
|
return Response(response_serializer.data)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
class AvailableDatesView(generics.GenericAPIView):
|
||||||
@permission_classes([AllowAny])
|
permission_classes = [AllowAny]
|
||||||
def available_dates(request):
|
|
||||||
availability = AdminWeeklyAvailability.objects.first()
|
|
||||||
if not availability:
|
|
||||||
return Response([])
|
|
||||||
|
|
||||||
available_days = availability.available_days
|
def get(self, request):
|
||||||
today = timezone.now().date()
|
availability = AdminWeeklyAvailability.objects.first()
|
||||||
available_dates = []
|
if not availability:
|
||||||
|
return Response([])
|
||||||
for i in range(1, 31):
|
|
||||||
date = today + timedelta(days=i)
|
available_days = availability.available_days
|
||||||
if date.weekday() in available_days:
|
today = timezone.now().date()
|
||||||
available_dates.append(date.strftime('%Y-%m-%d'))
|
available_dates = []
|
||||||
|
|
||||||
return Response(available_dates)
|
for i in range(1, 31):
|
||||||
|
date = today + timedelta(days=i)
|
||||||
|
if date.weekday() in available_days:
|
||||||
|
available_dates.append(date.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
|
return Response(available_dates)
|
||||||
|
|
||||||
@api_view(['GET'])
|
class UserAppointmentsView(generics.ListAPIView):
|
||||||
@permission_classes([IsAuthenticated])
|
permission_classes = [IsAuthenticated]
|
||||||
def user_appointments(request):
|
serializer_class = AppointmentRequestSerializer
|
||||||
appointments = AppointmentRequest.objects.filter(
|
|
||||||
email=request.user.email
|
def get_queryset(self):
|
||||||
).order_by('-created_at')
|
return AppointmentRequest.objects.filter(
|
||||||
|
email=self.request.user.email
|
||||||
|
).order_by('-created_at')
|
||||||
|
|
||||||
serializer = AppointmentRequestSerializer(appointments, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
class AppointmentStatsView(generics.GenericAPIView):
|
||||||
@permission_classes([IsAuthenticated])
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||||
def appointment_stats(request):
|
|
||||||
if not request.user.is_staff:
|
def get(self, request):
|
||||||
return Response(
|
total = AppointmentRequest.objects.count()
|
||||||
{'error': 'Unauthorized'},
|
users = CustomUser.objects.filter(is_staff=False).count()
|
||||||
status=status.HTTP_403_FORBIDDEN
|
pending = AppointmentRequest.objects.filter(status='pending_review').count()
|
||||||
|
scheduled = AppointmentRequest.objects.filter(status='scheduled').count()
|
||||||
|
rejected = AppointmentRequest.objects.filter(status='rejected').count()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'total_requests': total,
|
||||||
|
'pending_review': pending,
|
||||||
|
'scheduled': scheduled,
|
||||||
|
'rejected': rejected,
|
||||||
|
'users': users,
|
||||||
|
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0
|
||||||
|
})
|
||||||
|
|
||||||
|
class UserAppointmentStatsView(generics.GenericAPIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
stats = AppointmentRequest.objects.filter(
|
||||||
|
email=request.user.email
|
||||||
|
).aggregate(
|
||||||
|
total=Count('id'),
|
||||||
|
pending=Count('id', filter=Q(status='pending_review')),
|
||||||
|
scheduled=Count('id', filter=Q(status='scheduled')),
|
||||||
|
rejected=Count('id', filter=Q(status='rejected')),
|
||||||
|
completed=Count('id', filter=Q(status='completed'))
|
||||||
)
|
)
|
||||||
|
|
||||||
total = AppointmentRequest.objects.count()
|
total = stats['total']
|
||||||
users = CustomUser.objects.filter(is_staff=False).count()
|
scheduled = stats['scheduled']
|
||||||
pending = AppointmentRequest.objects.filter(status='pending_review').count()
|
completion_rate = round((scheduled / total * 100), 2) if total > 0 else 0
|
||||||
scheduled = AppointmentRequest.objects.filter(status='scheduled').count()
|
|
||||||
rejected = AppointmentRequest.objects.filter(status='rejected').count()
|
return Response({
|
||||||
|
'total_requests': total,
|
||||||
return Response({
|
'pending_review': stats['pending'],
|
||||||
'total_requests': total,
|
'scheduled': scheduled,
|
||||||
'pending_review': pending,
|
'rejected': stats['rejected'],
|
||||||
'scheduled': scheduled,
|
'completed': stats['completed'],
|
||||||
'rejected': rejected,
|
'completion_rate': completion_rate
|
||||||
'users': users,
|
})
|
||||||
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0
|
|
||||||
})
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
def user_apointment_stats(request):
|
|
||||||
if not request.user.is_staff:
|
|
||||||
return Response(
|
|
||||||
{'error': 'Unauthorized'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
total = AppointmentRequest.objects.filter(email=request.user.email).count()
|
|
||||||
pending = AppointmentRequest.objects.filter(email=request.user.email, status='pending_review').count()
|
|
||||||
scheduled = AppointmentRequest.objects.filter(email=request.user.email, status='scheduled').count()
|
|
||||||
rejected = AppointmentRequest.objects.filter(email=request.user.email, status='rejected').count()
|
|
||||||
completed = AppointmentRequest.objects.filter(email=request.user.email, status='completed').count()
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'total_requests': total,
|
|
||||||
'pending_review': pending,
|
|
||||||
'scheduled': scheduled,
|
|
||||||
'rejected': rejected,
|
|
||||||
'completed': completed,
|
|
||||||
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0
|
|
||||||
})
|
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
from rest_framework import status, generics
|
from rest_framework import status, generics
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from .models import CustomUser, UserProfile
|
from .models import CustomUser, UserProfile
|
||||||
@ -359,10 +359,6 @@ class UserDetailView(generics.RetrieveAPIView):
|
|||||||
def get_object(self):
|
def get_object(self):
|
||||||
return self.request.user
|
return self.request.user
|
||||||
|
|
||||||
|
|
||||||
def IsAdminUser(user):
|
|
||||||
return user.is_staff
|
|
||||||
|
|
||||||
class GetAllUsersView(generics.ListAPIView):
|
class GetAllUsersView(generics.ListAPIView):
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
permission_classes = [IsAdminUser, IsAuthenticated]
|
permission_classes = [IsAdminUser, IsAuthenticated]
|
||||||
@ -389,10 +385,8 @@ class DeleteUserView(generics.DestroyAPIView):
|
|||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
|
||||||
# Delete associated UserProfile
|
|
||||||
UserProfile.objects.filter(user=instance).delete()
|
UserProfile.objects.filter(user=instance).delete()
|
||||||
|
|
||||||
# Delete associated AppointmentRequests
|
|
||||||
AppointmentRequest.objects.filter(email=instance.email).delete()
|
AppointmentRequest.objects.filter(email=instance.email).delete()
|
||||||
|
|
||||||
class ActivateOrDeactivateUserView(generics.UpdateAPIView):
|
class ActivateOrDeactivateUserView(generics.UpdateAPIView):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user