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