diff --git a/booking_system/settings.py b/booking_system/settings.py index ecb05dc..75fd018 100644 --- a/booking_system/settings.py +++ b/booking_system/settings.py @@ -12,7 +12,7 @@ SECRET_KEY = os.getenv('JWT_SECRET', 'django-insecure-fallback-secret-key') DEBUG = os.getenv('DEBUG') -ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '*').split(',') +ALLOWED_HOSTS = ["*"] CORS_ALLOWED_ORIGINS = os.getenv( 'CORS_ALLOWED_ORIGINS', @@ -33,6 +33,7 @@ INSTALLED_APPS = [ 'rest_framework', 'rest_framework_simplejwt', 'corsheaders', + 'drf_spectacular', 'users', 'meetings', @@ -142,6 +143,7 @@ DEFAULT_FROM_EMAIL = os.getenv('SMTP_FROM', 'hello@attunehearttherapy.com') REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_AUTHENTICATION_CLASSES': ( '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 = [ "http://localhost:3000", "http://127.0.0.1:3000", diff --git a/booking_system/urls.py b/booking_system/urls.py index d8df884..fe4bd3d 100644 --- a/booking_system/urls.py +++ b/booking_system/urls.py @@ -1,10 +1,15 @@ from django.urls import path, include from django.contrib import admin from .views import api_root +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ path('admin/', admin.site.urls), path('api/auth/', include('users.urls')), path('api/meetings/', include('meetings.urls')), 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'), ] \ No newline at end of file diff --git a/meetings/urls.py b/meetings/urls.py index 3caf0e6..7857b81 100644 --- a/meetings/urls.py +++ b/meetings/urls.py @@ -4,12 +4,12 @@ from .views import ( AppointmentRequestListView, AppointmentRequestCreateView, AppointmentRequestDetailView, - schedule_appointment, - reject_appointment, - available_dates, - user_appointments, - appointment_stats, - user_apointment_stats + ScheduleAppointmentView, + RejectAppointmentView, + AvailableDatesView, + UserAppointmentsView, + AppointmentStatsView, + UserAppointmentStatsView ) urlpatterns = [ @@ -19,12 +19,12 @@ urlpatterns = [ path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'), path('appointments//', AppointmentRequestDetailView.as_view(), name='appointment-detail'), - path('appointments//schedule/', schedule_appointment, name='appointment-schedule'), - path('appointments//reject/', reject_appointment, name='appointment-reject'), + path('appointments//schedule/', ScheduleAppointmentView.as_view(), name='appointment-schedule'), + path('appointments//reject/', RejectAppointmentView.as_view(), name='appointment-reject'), - path('appointments/available-dates/', available_dates, name='available-dates'), - path('user/appointments/', user_appointments, name='user-appointments'), + path('appointments/available-dates/', AvailableDatesView.as_view(), name='available-dates'), + path('user/appointments/', UserAppointmentsView.as_view(), name='user-appointments'), - path('appointments/stats/', appointment_stats, name='appointment-stats'), - path('user/appointments/stats/', user_apointment_stats, name='user-appointment-stats'), + path('appointments/stats/', AppointmentStatsView.as_view(), name='appointment-stats'), + path('user/appointments/stats/', UserAppointmentStatsView.as_view(), name='user-appointment-stats'), ] \ No newline at end of file diff --git a/meetings/views.py b/meetings/views.py index 65564e8..b825749 100644 --- a/meetings/views.py +++ b/meetings/views.py @@ -1,7 +1,7 @@ from rest_framework import generics, status from rest_framework.decorators import api_view, permission_classes 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 datetime import datetime, timedelta from .models import AdminWeeklyAvailability, AppointmentRequest @@ -14,9 +14,11 @@ from .serializers import ( ) from .email_service import EmailService from users.models import CustomUser +from django.db.models import Count, Q + class AdminAvailabilityView(generics.RetrieveUpdateAPIView): - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated, IsAdminUser] serializer_class = AdminWeeklyAvailabilitySerializer def get_object(self): @@ -63,22 +65,24 @@ class AppointmentRequestDetailView(generics.RetrieveAPIView): serializer_class = AppointmentRequestSerializer lookup_field = 'pk' -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -def schedule_appointment(request, pk): - try: - appointment = AppointmentRequest.objects.get(pk=pk) - except AppointmentRequest.DoesNotExist: - return Response({'error': 'Appointment not found'}, status=status.HTTP_404_NOT_FOUND) +class ScheduleAppointmentView(generics.GenericAPIView): + permission_classes = [IsAuthenticated, IsAdminUser] + serializer_class = AppointmentScheduleSerializer + queryset = AppointmentRequest.objects.all() + lookup_field = 'pk' - if appointment.status != 'pending_review': - return Response( - {'error': 'Only pending review appointments can be scheduled.'}, - status=status.HTTP_400_BAD_REQUEST - ) - - serializer = AppointmentScheduleSerializer(data=request.data) - if serializer.is_valid(): + def post(self, request, pk): + appointment = self.get_object() + + if appointment.status != 'pending_review': + return Response( + {'error': 'Only pending review appointments can be scheduled.'}, + status=status.HTTP_400_BAD_REQUEST + ) + + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + appointment.schedule_appointment( datetime_obj=serializer.validated_data['scheduled_datetime'], duration=serializer.validated_data['scheduled_duration'] @@ -92,104 +96,106 @@ def schedule_appointment(request, pk): 'message': 'Appointment scheduled successfully. Jitsi meeting created.', 'jitsi_meeting_created': appointment.has_jitsi_meeting }) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -def reject_appointment(request, pk): - try: - appointment = AppointmentRequest.objects.get(pk=pk) - except AppointmentRequest.DoesNotExist: - return Response({'error': 'Appointment not found'}, status=status.HTTP_404_NOT_FOUND) +class RejectAppointmentView(generics.GenericAPIView): + permission_classes = [IsAuthenticated] + serializer_class = AppointmentRejectSerializer + queryset = AppointmentRequest.objects.all() + lookup_field = 'pk' - if appointment.status != 'pending_review': - return Response( - {'error': 'Only pending appointments can be rejected'}, - status=status.HTTP_400_BAD_REQUEST + def post(self, request, pk): + appointment = self.get_object() + + 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) - 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']) -@permission_classes([AllowAny]) -def available_dates(request): - availability = AdminWeeklyAvailability.objects.first() - if not availability: - return Response([]) +class AvailableDatesView(generics.GenericAPIView): + permission_classes = [AllowAny] - available_days = availability.available_days - today = timezone.now().date() - 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) + def get(self, request): + availability = AdminWeeklyAvailability.objects.first() + if not availability: + return Response([]) + + available_days = availability.available_days + today = timezone.now().date() + 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']) -@permission_classes([IsAuthenticated]) -def user_appointments(request): - appointments = AppointmentRequest.objects.filter( - email=request.user.email - ).order_by('-created_at') +class UserAppointmentsView(generics.ListAPIView): + permission_classes = [IsAuthenticated] + serializer_class = AppointmentRequestSerializer + + def get_queryset(self): + 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']) -@permission_classes([IsAuthenticated]) -def appointment_stats(request): - if not request.user.is_staff: - return Response( - {'error': 'Unauthorized'}, - status=status.HTTP_403_FORBIDDEN +class AppointmentStatsView(generics.GenericAPIView): + permission_classes = [IsAuthenticated, IsAdminUser] + + def get(self, request): + total = AppointmentRequest.objects.count() + users = CustomUser.objects.filter(is_staff=False).count() + 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() - users = CustomUser.objects.filter(is_staff=False).count() - 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 - }) - -@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 - }) + + total = stats['total'] + scheduled = stats['scheduled'] + completion_rate = round((scheduled / total * 100), 2) if total > 0 else 0 + + return Response({ + 'total_requests': total, + 'pending_review': stats['pending'], + 'scheduled': scheduled, + 'rejected': stats['rejected'], + 'completed': stats['completed'], + 'completion_rate': completion_rate + }) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d056f49..7057719 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/users/views.py b/users/views.py index a734481..5f8cea2 100644 --- a/users/views.py +++ b/users/views.py @@ -1,7 +1,7 @@ from rest_framework import status, generics from rest_framework.decorators import api_view, permission_classes 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 django.contrib.auth import authenticate from .models import CustomUser, UserProfile @@ -359,10 +359,6 @@ class UserDetailView(generics.RetrieveAPIView): def get_object(self): return self.request.user - -def IsAdminUser(user): - return user.is_staff - class GetAllUsersView(generics.ListAPIView): serializer_class = UserSerializer permission_classes = [IsAdminUser, IsAuthenticated] @@ -389,10 +385,8 @@ class DeleteUserView(generics.DestroyAPIView): def perform_destroy(self, instance): instance.delete() - # Delete associated UserProfile UserProfile.objects.filter(user=instance).delete() - # Delete associated AppointmentRequests AppointmentRequest.objects.filter(email=instance.email).delete() class ActivateOrDeactivateUserView(generics.UpdateAPIView):