Compare commits

..

2 Commits

Author SHA1 Message Date
8f5a97f18e Merge pull request 'feat: add user management endpoints and update appointment model' (#10) from feature/meetings into main
Reviewed-on: https://gitea.blackbusinesslabs.com/ATTUNE-HEART-THERAPY/alternative-backend-service/pulls/10
2025-11-23 13:55:48 +00:00
4fdc7c35ee 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
2025-11-23 13:55:04 +00:00
11 changed files with 163 additions and 51 deletions

View File

@ -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/<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)",
"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"

View File

@ -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)),
],

View File

@ -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'),
),
]

View 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'),
),
]

View 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',
),
]

View File

@ -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()

View File

@ -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
})

View File

@ -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

View File

@ -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')
fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined', 'last_login', 'is_staff', 'is_superuser', 'is_active')

View File

@ -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/<uuid:pk>/", views.ActivateOrDeactivateUserView.as_view(), name="activate-deactivate-user"),
path("delete-user/<uuid:pk>/", views.DeleteUserView.as_view(), name="delete-user"),
]

View File

@ -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
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)