Compare commits
2 Commits
61a703c7c2
...
4b86761ddc
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b86761ddc | |||
| 1ffbfa5692 |
@ -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",
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
@ -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/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'),
|
||||
|
||||
path('appointments/<uuid:pk>/schedule/', schedule_appointment, name='appointment-schedule'),
|
||||
path('appointments/<uuid:pk>/reject/', reject_appointment, name='appointment-reject'),
|
||||
path('appointments/<uuid:pk>/schedule/', ScheduleAppointmentView.as_view(), name='appointment-schedule'),
|
||||
path('appointments/<uuid:pk>/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'),
|
||||
]
|
||||
@ -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
|
||||
})
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user