diff --git a/backend/core/urls.py b/backend/core/urls.py index 0a2ba39..919be86 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -20,8 +20,8 @@ urlpatterns = [ path("admin/", admin.site.urls), - # API Routes - Uncomment when ready to use - # path('api/', include('users.urls')), + # API Routes + path("api/", include("users.urls")), # path('api/inventory/', include('inventory.urls')), # path('api/', include('requests.urls')), ] diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 9de63fd..9e2d6fb 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -1,33 +1,21 @@ from rest_framework import serializers +from .models import VolunteerApplication -# from .models import User -# Create your serializers here. +class VolunteerApplicationSerializer(serializers.ModelSerializer): + """ + Serializer for the VolunteerApplication model. + Handles creating new volunteer applications upon submission. + """ -# Example: User Serializer -# Uncomment and modify as needed -# -# class UserSerializer(serializers.ModelSerializer): -# """ -# Serializer for the User model. -# Controls what fields are exposed in the API. -# """ -# class Meta: -# model = User -# fields = ['id', 'username', 'email', 'first_name', 'last_name', 'date_joined'] -# read_only_fields = ['id', 'date_joined'] -# -# -# class UserCreateSerializer(serializers.ModelSerializer): -# """ -# Serializer for creating new users with password handling. -# """ -# password = serializers.CharField(write_only=True, min_length=8, style={'input_type': 'password'}) -# -# class Meta: -# model = User -# fields = ['username', 'email', 'password', 'first_name', 'last_name'] -# -# def create(self, validated_data): -# """Create user with encrypted password.""" -# return User.objects.create_user(**validated_data) + reviewed_by = serializers.StringRelatedField(read_only=True) + + class Meta: + model = VolunteerApplication + fields = ["id", "name", "email", "motivation_text", "status", "created_at", "reviewed_at", "reviewed_by"] + read_only_fields = ["id", "created_at", "reviewed_at", "reviewed_by"] + + def create(self, validated_data): + """Create a new volunteer application with PENDING status.""" + validated_data["status"] = "PENDING" + return super().create(validated_data) diff --git a/backend/users/tests.py b/backend/users/tests.py index 2e221d4..cff1371 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -1,6 +1,10 @@ import pytest +from django.test import Client from django.contrib.auth import get_user_model +from .models import VolunteerApplication + + User = get_user_model() @@ -11,3 +15,50 @@ def test_create_user(): assert user.email == "test@example.com" assert user.name == "Test User" assert user.check_password("testpass123") + + +@pytest.mark.django_db +def test_approve_volunteer_application(): + client = Client() + + application = VolunteerApplication.objects.create( + name="Test Volunteer", + email="volunteer@example.com", + motivation_text="I want to help", + status="PENDING", + ) + + url = f"/api/volunteer-applications/{application.id}/" + + response = client.patch(url, {"status": "APPROVED"}, content_type="application/json") + assert response.status_code in (200, 202) + + application.refresh_from_db() + assert application.status == "APPROVED" + assert application.reviewed_at is not None + + user = User.objects.get(email="volunteer@example.com") + assert user.role == "VOLUNTEER" + + +@pytest.mark.django_db +def test_reject_application(): + client = Client() + + application = VolunteerApplication.objects.create( + name="Test Volunteer 2", + email="reject@example.com", + motivation_text="I want to help", + status="PENDING", + ) + + url = f"/api/volunteer-applications/{application.id}/" + + response = client.patch(url, {"status": "REJECTED"}, content_type="application/json") + assert response.status_code in (200, 202) + + application.refresh_from_db() + assert application.status == "REJECTED" + assert application.reviewed_at is not None + + assert not User.objects.filter(email="reject@example.com").exists() diff --git a/backend/users/urls.py b/backend/users/urls.py index 3a04e3e..ff0f5a5 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,27 +1,19 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter - -# from .views import UserViewSet +from .views import VolunteerApplicationAPIView # Create your URL patterns here. -# Example: Using DRF Router for ViewSets -# Uncomment and modify as needed -# -# router = DefaultRouter() -# router.register(r'users', UserViewSet, basename='user') -# -# urlpatterns = [ -# path('', include(router.urls)), -# ] -# This will create the following endpoints: -# GET /api/users/ - List all users -# POST /api/users/ - Create a new user -# GET /api/users/{id}/ - Retrieve a specific user -# PUT /api/users/{id}/ - Update a user -# PATCH /api/users/{id}/ - Partial update -# DELETE /api/users/{id}/ - Delete a user -# GET /api/users/me/ - Custom action (if defined in viewset) +router = DefaultRouter() +router.register(r"volunteer-applications", VolunteerApplicationAPIView, basename="volunteer-application") -urlpatterns = [] +urlpatterns = [ + path("", include(router.urls)), +] + +# This will create the following endpoints: +# POST /api/volunteer-applications/ - Create a new application (PENDING) +# GET /api/volunteer-applications/ - List all applications +# GET /api/volunteer-applications/{id}/ - Retrieve a specific application (admin only) +# PATCH /api/volunteer-applications/{id}/ - Partial update diff --git a/backend/users/views.py b/backend/users/views.py index d228035..56dc2cf 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,35 +1,80 @@ from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response +from django.contrib.auth import get_user_model +from django.utils import timezone +from django.db import transaction +import secrets -# from .models import User -# from .serializers import UserSerializer, UserCreateSerializer - -# Create your views here. - -# Example: User ViewSet -# Uncomment and modify as needed -# -# class UserViewSet(viewsets.ModelViewSet): -# """ -# ViewSet for managing users. -# Provides CRUD operations: list, create, retrieve, update, delete -# """ -# queryset = User.objects.all() -# serializer_class = UserSerializer -# permission_classes = [permissions.IsAuthenticatedOrReadOnly] -# -# def get_serializer_class(self): -# """Return appropriate serializer based on action.""" -# if self.action == 'create': -# return UserCreateSerializer -# return UserSerializer -# -# @action(detail=False, methods=['get'], permission_classes=[permissions.IsAuthenticated]) -# def me(self, request): -# """ -# Custom endpoint: GET /api/users/me/ -# Returns the current authenticated user. -# """ -# serializer = self.get_serializer(request.user) -# return Response(serializer.data) +from .models import VolunteerApplication +from .serializers import VolunteerApplicationSerializer + +User = get_user_model() + + +class VolunteerApplicationAPIView(viewsets.ModelViewSet): + """ + API endpoint for submitting volunteer applications. + + Currently contains: + POST /api/volunteer-applications/ + """ + + queryset = VolunteerApplication.objects.all() + serializer_class = VolunteerApplicationSerializer + permission_classes = [permissions.AllowAny] + + def get_serializer_class(self): + """Return appropriate serializer based on action.""" + if self.action == "create": + return VolunteerApplicationSerializer + return VolunteerApplicationSerializer # Can update this later, just for formality + + def list(self, request, *args, **kwargs): + """List applications; restrict to admin users using role PLACEHOLDER.""" + user = getattr(request, "user", None) + if not (user is not None and getattr(user, "is_authenticated", False) and self._is_admin(user)): + return Response({"detail": "Admin only"}, status=status.HTTP_403_FORBIDDEN) + + return super().list(request, *args, **kwargs) + + def _is_admin(self, user): + """PLACEHOLDER check for admin users based on role.""" + return getattr(user, "role", None) == "ADMIN" + + def _handle_review_metadata(self, application): + """Set reviewed_at and reviewed_by when an application is reviewed.""" + if application.reviewed_at is None: + application.reviewed_at = timezone.now() + + user = getattr(self.request, "user", None) + if user is not None and getattr(user, "is_authenticated", False) and application.reviewed_by is None: + application.reviewed_by = user + + application.save() + + def _handle_volunteer_user_creation(self, application): + """Create a VOLUNTEER user when an application is approved, if needed.""" + if application.status != "APPROVED": + return + + exists = User.objects.filter(email=application.email).exists() + if exists: + return + + temp_password = secrets.token_urlsafe(12) + User.objects.create_user( + email=application.email, + name=application.name, + password=temp_password, + role="VOLUNTEER", + ) + + @transaction.atomic + def perform_update(self, serializer): + old_status = serializer.instance.status + application = serializer.save() + + if old_status != application.status and application.status in {"APPROVED", "REJECTED"}: + self._handle_review_metadata(application) + self._handle_volunteer_user_creation(application)