feat: add user management endpoints and update appointment model
Add comprehensive API documentation for user management endpoints including profile updates, user listing, and admin user management features. Update appointment model to include additional status options (completed, cancelled) and add max_length constraint to email field. Change appointment creation endpoint to require user authentication instead of being public. Changes: - Add API docs for update_profile, get_profile, all-users endpoints - Add API docs for activate-deactivate-user and delete-user admin endpoints - Update appointment creation to require authentication - Add 'completed' and 'cancelled' status options to Appointment model - Add max_length constraint to EncryptedEmailField - Regenerate initial migration with updated model definitions
This commit is contained in:
parent
4acd78988e
commit
4fdc7c35ee
@ -96,6 +96,54 @@ def api_root(request, format=None):
|
|||||||
'example_request': {
|
'example_request': {
|
||||||
'refresh': 'your_refresh_token_here'
|
'refresh': 'your_refresh_token_here'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"update_profile": {
|
||||||
|
"description": "Update user profile (Authenticated users only)",
|
||||||
|
"url": request.build_absolute_uri("/api/auth/profile/update/"),
|
||||||
|
"methods": ["PATCH"],
|
||||||
|
"authentication": "Required (Authenticated users only)",
|
||||||
|
"required_fields": ["first_name", "last_name", "phone_number"],
|
||||||
|
"example_request": {
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe",
|
||||||
|
"phone_number": "+1234567890"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get_profile": {
|
||||||
|
"description": "Get user profile (Authenticated users only)",
|
||||||
|
"url": request.build_absolute_uri("/api/auth/profile/"),
|
||||||
|
"methods": ["GET"],
|
||||||
|
"authentication": "Required (Authenticated users only)",
|
||||||
|
"response_fields": {
|
||||||
|
"user": "User object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"all-users": {
|
||||||
|
"description": "Get all users (Admin only)",
|
||||||
|
"url": request.build_absolute_uri("/api/auth/all-users/"),
|
||||||
|
"methods": ["GET"],
|
||||||
|
"authentication": "Required (Admin users only)",
|
||||||
|
"response_fields": {
|
||||||
|
"users": "List of user objects"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"activate-deactivate-user": {
|
||||||
|
"description": "Activate or deactivate a user (Admin only)",
|
||||||
|
"url": request.build_absolute_uri("/api/auth/activate-deactivate-user/<uuid:pk>/"),
|
||||||
|
"methods": ["GET"],
|
||||||
|
"authentication": "Required (Admin users only)",
|
||||||
|
"response_fields": {
|
||||||
|
"user": "User object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete-user": {
|
||||||
|
"description": "Delete a user (Admin only)",
|
||||||
|
"url": request.build_absolute_uri("/api/auth/delete-user/<uuid:pk>/"),
|
||||||
|
"methods": ["GET"],
|
||||||
|
"authentication": "Required (Admin users only)",
|
||||||
|
"response_fields": {
|
||||||
|
"user": "User object"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -127,7 +175,7 @@ def api_root(request, format=None):
|
|||||||
"description": "Create a new appointment request (Public)",
|
"description": "Create a new appointment request (Public)",
|
||||||
"url": request.build_absolute_uri("/api/meetings/appointments/create/"),
|
"url": request.build_absolute_uri("/api/meetings/appointments/create/"),
|
||||||
"methods": ["POST"],
|
"methods": ["POST"],
|
||||||
"authentication": "None required",
|
"authentication": "Required (User only)",
|
||||||
"required_fields": [
|
"required_fields": [
|
||||||
"first_name", "last_name", "email",
|
"first_name", "last_name", "email",
|
||||||
"preferred_dates", "preferred_time_slots"
|
"preferred_dates", "preferred_time_slots"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.8 on 2025-11-22 22:06
|
# Generated by Django 5.2.8 on 2025-11-23 12:42
|
||||||
|
|
||||||
import meetings.models
|
import meetings.models
|
||||||
import uuid
|
import uuid
|
||||||
@ -32,14 +32,17 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
('first_name', meetings.models.EncryptedCharField(max_length=100)),
|
('first_name', meetings.models.EncryptedCharField(max_length=100)),
|
||||||
('last_name', meetings.models.EncryptedCharField(max_length=100)),
|
('last_name', meetings.models.EncryptedCharField(max_length=100)),
|
||||||
('email', meetings.models.EncryptedEmailField()),
|
('email', meetings.models.EncryptedEmailField(max_length=254)),
|
||||||
('phone', meetings.models.EncryptedCharField(blank=True, max_length=20)),
|
('phone', meetings.models.EncryptedCharField(blank=True, max_length=20)),
|
||||||
('reason', meetings.models.EncryptedTextField(blank=True)),
|
('reason', meetings.models.EncryptedTextField(blank=True)),
|
||||||
('preferred_dates', models.JSONField(help_text='List of preferred dates (YYYY-MM-DD format)')),
|
('preferred_dates', models.JSONField(help_text='List of preferred dates (YYYY-MM-DD format)')),
|
||||||
('preferred_time_slots', models.JSONField(help_text='List of preferred time slots (morning/afternoon/evening)')),
|
('preferred_time_slots', models.JSONField(help_text='List of preferred time slots (morning/afternoon/evening)')),
|
||||||
('status', models.CharField(choices=[('pending_review', 'Pending Review'), ('scheduled', 'Scheduled'), ('rejected', 'Rejected')], default='pending_review', max_length=20)),
|
('status', models.CharField(choices=[('pending_review', 'Pending Review'), ('scheduled', 'Scheduled'), ('rejected', 'Rejected'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending_review', max_length=20)),
|
||||||
('scheduled_datetime', models.DateTimeField(blank=True, null=True)),
|
('scheduled_datetime', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('scheduled_duration', models.PositiveIntegerField(default=60, help_text='Duration in minutes')),
|
||||||
('rejection_reason', meetings.models.EncryptedTextField(blank=True)),
|
('rejection_reason', meetings.models.EncryptedTextField(blank=True)),
|
||||||
|
('jitsi_meet_url', models.URLField(blank=True, help_text='Jitsi Meet URL for the video session')),
|
||||||
|
('jitsi_room_id', models.CharField(blank=True, help_text='Jitsi room ID', max_length=100, unique=True)),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,41 +0,0 @@
|
|||||||
# Generated by Django 5.2.8 on 2025-11-22 23:31
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('meetings', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='appointmentrequest',
|
|
||||||
name='jitsi_meet_url',
|
|
||||||
field=models.URLField(blank=True, help_text='Jitsi Meet URL for the video session'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='appointmentrequest',
|
|
||||||
name='jitsi_room_id',
|
|
||||||
field=models.CharField(blank=True, help_text='Jitsi room ID', max_length=100, unique=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='appointmentrequest',
|
|
||||||
name='scheduled_duration',
|
|
||||||
field=models.PositiveIntegerField(default=60, help_text='Duration in minutes'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='appointmentrequest',
|
|
||||||
name='status',
|
|
||||||
field=models.CharField(choices=[('pending_review', 'Pending Review'), ('scheduled', 'Scheduled'), ('rejected', 'Rejected'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending_review', max_length=20),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='appointmentrequest',
|
|
||||||
index=models.Index(fields=['status', 'scheduled_datetime'], name='meetings_ap_status_4e4e26_idx'),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='appointmentrequest',
|
|
||||||
index=models.Index(fields=['email', 'created_at'], name='meetings_ap_email_b8ed9d_idx'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
31
meetings/migrations/0002_initial.py
Normal file
31
meetings/migrations/0002_initial.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-11-23 12:42
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('meetings', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='appointmentrequest',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='appointmentrequest',
|
||||||
|
index=models.Index(fields=['status', 'scheduled_datetime'], name='meetings_ap_status_4e4e26_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='appointmentrequest',
|
||||||
|
index=models.Index(fields=['email', 'created_at'], name='meetings_ap_email_b8ed9d_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
meetings/migrations/0003_remove_appointmentrequest_user.py
Normal file
17
meetings/migrations/0003_remove_appointmentrequest_user.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-11-23 12:48
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('meetings', '0002_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='appointmentrequest',
|
||||||
|
name='user',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -121,7 +121,6 @@ class AppointmentRequest(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
|
||||||
first_name = EncryptedCharField(max_length=100)
|
first_name = EncryptedCharField(max_length=100)
|
||||||
last_name = EncryptedCharField(max_length=100)
|
last_name = EncryptedCharField(max_length=100)
|
||||||
email = EncryptedEmailField()
|
email = EncryptedEmailField()
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from .serializers import (
|
|||||||
AppointmentRejectSerializer
|
AppointmentRejectSerializer
|
||||||
)
|
)
|
||||||
from .email_service import EmailService
|
from .email_service import EmailService
|
||||||
|
from users.models import CustomUser
|
||||||
|
|
||||||
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
|
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
@ -37,7 +38,7 @@ class AppointmentRequestListView(generics.ListAPIView):
|
|||||||
return queryset.filter(email=self.request.user.email)
|
return queryset.filter(email=self.request.user.email)
|
||||||
|
|
||||||
class AppointmentRequestCreateView(generics.CreateAPIView):
|
class AppointmentRequestCreateView(generics.CreateAPIView):
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [IsAuthenticated]
|
||||||
queryset = AppointmentRequest.objects.all()
|
queryset = AppointmentRequest.objects.all()
|
||||||
serializer_class = AppointmentRequestCreateSerializer
|
serializer_class = AppointmentRequestCreateSerializer
|
||||||
|
|
||||||
@ -155,6 +156,7 @@ def appointment_stats(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
total = AppointmentRequest.objects.count()
|
total = AppointmentRequest.objects.count()
|
||||||
|
users = CustomUser.objects.filter(is_staff=False).count()
|
||||||
pending = AppointmentRequest.objects.filter(status='pending_review').count()
|
pending = AppointmentRequest.objects.filter(status='pending_review').count()
|
||||||
scheduled = AppointmentRequest.objects.filter(status='scheduled').count()
|
scheduled = AppointmentRequest.objects.filter(status='scheduled').count()
|
||||||
rejected = AppointmentRequest.objects.filter(status='rejected').count()
|
rejected = AppointmentRequest.objects.filter(status='rejected').count()
|
||||||
@ -164,5 +166,6 @@ def appointment_stats(request):
|
|||||||
'pending_review': pending,
|
'pending_review': pending,
|
||||||
'scheduled': scheduled,
|
'scheduled': scheduled,
|
||||||
'rejected': rejected,
|
'rejected': rejected,
|
||||||
|
'users': users,
|
||||||
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0
|
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0
|
||||||
})
|
})
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.8 on 2025-11-22 22:06
|
# Generated by Django 5.2.8 on 2025-11-23 12:42
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
@ -53,4 +53,4 @@ class ResetPasswordSerializer(serializers.Serializer):
|
|||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined')
|
fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined', 'last_login', 'is_staff', 'is_superuser', 'is_active')
|
||||||
@ -19,5 +19,8 @@ urlpatterns = [
|
|||||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
path('profile/', views.get_user_profile, name='profile'),
|
path('profile/', views.get_user_profile, name='profile'),
|
||||||
path('profile/update/', views.update_user_profile, name='update_profile'),
|
path('profile/update/', views.update_user_profile, name='update_profile'),
|
||||||
path('me/', views.UserDetailView.as_view(), name='user_detail'),
|
|
||||||
|
path("all-users/", views.GetAllUsersView.as_view(), name="all-users"),
|
||||||
|
path("activate-deactivate-user/<uuid:pk>/", views.ActivateOrDeactivateUserView.as_view(), name="activate-deactivate-user"),
|
||||||
|
path("delete-user/<uuid:pk>/", views.DeleteUserView.as_view(), name="delete-user"),
|
||||||
]
|
]
|
||||||
@ -10,6 +10,7 @@ from .utils import send_otp_via_email, is_otp_expired, generate_otp
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
from meetings.models import AppointmentRequest
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@ -356,4 +357,52 @@ class UserDetailView(generics.RetrieveAPIView):
|
|||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
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):
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [IsAdminUser, IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return CustomUser.objects.filter(is_staff=False)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteUserView(generics.DestroyAPIView):
|
||||||
|
queryset = CustomUser.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [IsAdminUser, IsAuthenticated]
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
self.perform_destroy(instance)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
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):
|
||||||
|
queryset = CustomUser.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [IsAdminUser, IsAuthenticated]
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
instance.is_active = not instance.is_active
|
||||||
|
instance.save()
|
||||||
|
serializer = self.get_serializer(instance)
|
||||||
|
return Response(serializer.data)
|
||||||
Loading…
Reference in New Issue
Block a user