From 4fdc7c35ee9c0965898b3b3a5241604204dd7253 Mon Sep 17 00:00:00 2001 From: saani Date: Sun, 23 Nov 2025 13:55:04 +0000 Subject: [PATCH] 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 --- booking_system/views.py | 50 +++++++++++++++++- meetings/migrations/0001_initial.py | 9 ++-- ...ointmentrequest_jitsi_meet_url_and_more.py | 41 --------------- meetings/migrations/0002_initial.py | 31 +++++++++++ .../0003_remove_appointmentrequest_user.py | 17 +++++++ meetings/models.py | 1 - meetings/views.py | 5 +- users/migrations/0001_initial.py | 2 +- users/serializers.py | 2 +- users/urls.py | 5 +- users/views.py | 51 ++++++++++++++++++- 11 files changed, 163 insertions(+), 51 deletions(-) delete mode 100644 meetings/migrations/0002_appointmentrequest_jitsi_meet_url_and_more.py create mode 100644 meetings/migrations/0002_initial.py create mode 100644 meetings/migrations/0003_remove_appointmentrequest_user.py diff --git a/booking_system/views.py b/booking_system/views.py index 6e1e8c2..68933e9 100644 --- a/booking_system/views.py +++ b/booking_system/views.py @@ -96,6 +96,54 @@ def api_root(request, format=None): 'example_request': { '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//"), + "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//"), + "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)", "url": request.build_absolute_uri("/api/meetings/appointments/create/"), "methods": ["POST"], - "authentication": "None required", + "authentication": "Required (User only)", "required_fields": [ "first_name", "last_name", "email", "preferred_dates", "preferred_time_slots" diff --git a/meetings/migrations/0001_initial.py b/meetings/migrations/0001_initial.py index 72a8f26..0a0596c 100644 --- a/meetings/migrations/0001_initial.py +++ b/meetings/migrations/0001_initial.py @@ -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 uuid @@ -32,14 +32,17 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('first_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)), ('reason', meetings.models.EncryptedTextField(blank=True)), ('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)')), - ('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_duration', models.PositiveIntegerField(default=60, help_text='Duration in minutes')), ('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)), ('updated_at', models.DateTimeField(auto_now=True)), ], diff --git a/meetings/migrations/0002_appointmentrequest_jitsi_meet_url_and_more.py b/meetings/migrations/0002_appointmentrequest_jitsi_meet_url_and_more.py deleted file mode 100644 index 2bac344..0000000 --- a/meetings/migrations/0002_appointmentrequest_jitsi_meet_url_and_more.py +++ /dev/null @@ -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'), - ), - ] diff --git a/meetings/migrations/0002_initial.py b/meetings/migrations/0002_initial.py new file mode 100644 index 0000000..d36ac72 --- /dev/null +++ b/meetings/migrations/0002_initial.py @@ -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'), + ), + ] diff --git a/meetings/migrations/0003_remove_appointmentrequest_user.py b/meetings/migrations/0003_remove_appointmentrequest_user.py new file mode 100644 index 0000000..4b603c6 --- /dev/null +++ b/meetings/migrations/0003_remove_appointmentrequest_user.py @@ -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', + ), + ] diff --git a/meetings/models.py b/meetings/models.py index 31a5391..0de0793 100644 --- a/meetings/models.py +++ b/meetings/models.py @@ -121,7 +121,6 @@ class AppointmentRequest(models.Model): ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - first_name = EncryptedCharField(max_length=100) last_name = EncryptedCharField(max_length=100) email = EncryptedEmailField() diff --git a/meetings/views.py b/meetings/views.py index c001810..0a93776 100644 --- a/meetings/views.py +++ b/meetings/views.py @@ -13,6 +13,7 @@ from .serializers import ( AppointmentRejectSerializer ) from .email_service import EmailService +from users.models import CustomUser class AdminAvailabilityView(generics.RetrieveUpdateAPIView): permission_classes = [IsAuthenticated] @@ -37,7 +38,7 @@ class AppointmentRequestListView(generics.ListAPIView): return queryset.filter(email=self.request.user.email) class AppointmentRequestCreateView(generics.CreateAPIView): - permission_classes = [AllowAny] + permission_classes = [IsAuthenticated] queryset = AppointmentRequest.objects.all() serializer_class = AppointmentRequestCreateSerializer @@ -155,6 +156,7 @@ def appointment_stats(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() @@ -164,5 +166,6 @@ def appointment_stats(request): 'pending_review': pending, 'scheduled': scheduled, 'rejected': rejected, + 'users': users, 'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0 }) \ No newline at end of file diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index d03ee6e..8e4ec64 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -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 from django.conf import settings diff --git a/users/serializers.py b/users/serializers.py index 7bd00d1..55a2108 100644 --- a/users/serializers.py +++ b/users/serializers.py @@ -53,4 +53,4 @@ class ResetPasswordSerializer(serializers.Serializer): class UserSerializer(serializers.ModelSerializer): class Meta: model = CustomUser - fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined') \ No newline at end of file + fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined', 'last_login', 'is_staff', 'is_superuser', 'is_active') \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index f077739..42984ec 100644 --- a/users/urls.py +++ b/users/urls.py @@ -19,5 +19,8 @@ urlpatterns = [ path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('profile/', views.get_user_profile, name='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//", views.ActivateOrDeactivateUserView.as_view(), name="activate-deactivate-user"), + path("delete-user//", views.DeleteUserView.as_view(), name="delete-user"), ] \ No newline at end of file diff --git a/users/views.py b/users/views.py index e0b58fe..a734481 100644 --- a/users/views.py +++ b/users/views.py @@ -10,6 +10,7 @@ from .utils import send_otp_via_email, is_otp_expired, generate_otp from django.utils import timezone from datetime import timedelta from rest_framework.reverse import reverse +from meetings.models import AppointmentRequest @api_view(['POST']) @@ -356,4 +357,52 @@ class UserDetailView(generics.RetrieveAPIView): permission_classes = [IsAuthenticated] def get_object(self): - return self.request.user \ No newline at end of file + 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) \ No newline at end of file -- 2.39.5