-
Notifications
You must be signed in to change notification settings - Fork 0
feat(backend): add volunteer application endpoints #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a volunteer application submission and review system, enabling public users to submit applications and admins to approve/reject them. When approved, the system automatically creates a volunteer user account. The implementation adds REST API endpoints for volunteer application CRUD operations, though several security and design issues need to be addressed before merging.
Key Changes
- Added
VolunteerApplicationAPIView(ModelViewSet) with endpoints for creating, listing, retrieving, and updating volunteer applications - Implemented automatic user account creation with role "VOLUNTEER" upon application approval
- Added approval workflow tracking with
reviewed_atandreviewed_byfields
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/users/views.py | Implements the volunteer application viewset with submission, listing (admin-only), and approval/rejection logic with automatic user creation |
| backend/users/serializers.py | Adds serializer for volunteer applications, forcing new applications to PENDING status |
| backend/users/urls.py | Registers the volunteer application endpoints with the router |
| backend/users/tests.py | Adds tests for application approval/rejection flows and user creation |
| backend/core/urls.py | Enables the users API routes by uncommenting the include statement |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # 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) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states "Retrieve a specific application (admin only)" but there's no admin-only restriction implemented for the retrieve endpoint. The current implementation only protects the list endpoint. Either update the comment to reflect the actual implementation or add the missing admin protection to the retrieve endpoint.
| # GET /api/volunteer-applications/{id}/ - Retrieve a specific application (admin only) | |
| # GET /api/volunteer-applications/{id}/ - Retrieve a specific application |
|
|
||
| queryset = VolunteerApplication.objects.all() | ||
| serializer_class = VolunteerApplicationSerializer | ||
| permission_classes = [permissions.AllowAny] |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AllowAny permission class is too permissive for a ModelViewSet. This allows unrestricted access to all CRUD operations (GET, POST, PUT, PATCH, DELETE) on all volunteer applications, including retrieving and modifying individual applications. Consider using DRF's get_permissions() method to return different permission classes based on the action (e.g., AllowAny for create, but require authentication/admin for list, retrieve, update, destroy).
|
|
||
|
|
||
| @pytest.mark.django_db | ||
| def test_approve_volunteer_application(): |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for the public POST endpoint to create volunteer applications. Add a test that verifies: 1) Anyone can submit an application via POST, 2) The application is created with status="PENDING", and 3) The response includes the expected fields.
| @transaction.atomic | ||
| def perform_update(self, serializer): | ||
| old_status = serializer.instance.status | ||
| application = serializer.save() | ||
|
|
||
| if old_status == "PENDING" and application.status in {"APPROVED", "REJECTED"}: | ||
| self._handle_review_metadata(application) | ||
| self._handle_volunteer_user_creation(application) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using PATCH on the main resource for approval/rejection is not RESTful best practice. Consider implementing dedicated action endpoints (e.g., @action(detail=True, methods=['post']) def approve(...) and @action(detail=True, methods=['post']) def reject(...)) that would create endpoints like POST /api/volunteer-applications/{id}/approve/ and POST /api/volunteer-applications/{id}/reject/. This makes the API more explicit and allows for additional approval/rejection metadata (e.g., rejection reasons) in the request body.
| 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 | ||
|
|
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The get_serializer_class() method always returns the same serializer regardless of action, making it redundant. Either remove this method entirely (and rely on the serializer_class attribute) or add a meaningful distinction between actions if different serializers are needed in the future.
| 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 |
| if user is not None and getattr(user, "is_authenticated", False) and application.reviewed_by is None: | ||
| application.reviewed_by = user | ||
|
|
||
| application.save() |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _handle_review_metadata() method calls application.save() which saves the application outside the serializer's save flow. Since this method is called after serializer.save() in perform_update(), it performs a second database write. This can cause issues with the transaction and the fields might not be properly updated through the serializer. Instead, set the fields before calling serializer.save() or pass them to the serializer's save method.
| 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"] |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The status field is exposed as writable in the serializer, allowing public users to submit applications with any status (including "APPROVED" or "REJECTED") by including the status in the POST request body. Even though the create() method forces status to "PENDING", the field should be added to read_only_fields to make the API contract clearer and prevent status manipulation on updates.
| read_only_fields = ["id", "created_at", "reviewed_at", "reviewed_by"] | |
| read_only_fields = ["id", "created_at", "reviewed_at", "reviewed_by", "status"] |
|
|
||
|
|
||
| @pytest.mark.django_db | ||
| def test_approve_volunteer_application(): |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for edge cases: 1) Approving an application when a user with the same email already exists (should not create duplicate), 2) Attempting to change status from APPROVED back to PENDING or REJECTED (should review metadata behavior), 3) Directly updating to APPROVED without going through PENDING first.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
backend/core/settings.py
Outdated
| # Secret key fallback for pytest on github | ||
| if not SECRET_KEY: | ||
| SECRET_KEY = "test-secret-key" |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a hardcoded SECRET_KEY as a fallback is a security risk. If the environment variable is not set in production (e.g., due to misconfiguration), Django will use this weak, publicly-visible key, making the application vulnerable to session hijacking, CSRF attacks, and other security issues.
Instead of a fallback, the application should fail explicitly when SECRET_KEY is not set:
if not SECRET_KEY:
raise ValueError("SECRET_KEY environment variable must be set")If a fallback is needed specifically for testing, use pytest configuration (e.g., conftest.py) to set it only in test environments.
| # Secret key fallback for pytest on github | |
| if not SECRET_KEY: | |
| SECRET_KEY = "test-secret-key" | |
| # Ensure SECRET_KEY is set | |
| if not SECRET_KEY: | |
| raise ValueError("SECRET_KEY environment variable must be set") |
| @@ -1,35 +1,80 @@ | |||
| from rest_framework import viewsets, permissions, status | |||
| from rest_framework.decorators import action | |||
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The action decorator is imported but never used in this file. Remove this unused import to keep the code clean.
| from rest_framework.decorators import action |
| application.save() | ||
|
|
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The application.save() call causes the application to be saved twice within the same transaction: once by serializer.save() (line 76) and again here. This is inefficient and could lead to race conditions or unexpected behavior.
Instead, modify the fields without calling save() and let the transaction commit handle the single save operation. The @transaction.atomic decorator will ensure all changes are committed together.
| application.save() |
| assert user.check_password("testpass123") | ||
|
|
||
|
|
||
| @pytest.mark.django_db |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for the public POST endpoint (/api/volunteer-applications/) that allows unauthenticated users to submit volunteer applications. This is a core feature mentioned in the PR description.
Add a test that:
- POSTs a new application without authentication
- Verifies the response status is 201
- Confirms the application is created with
status="PENDING" - Ensures
reviewed_atandreviewed_byare null for new submissions
| application.refresh_from_db() | ||
| assert application.status == "APPROVED" | ||
| assert application.reviewed_at is not None | ||
|
|
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verifies that reviewed_at is set, but doesn't verify that reviewed_by is also set when an application is approved. According to the PR description and the _handle_review_metadata implementation, both fields should be populated when an authenticated admin reviews an application.
Add an assertion to verify application.reviewed_by is set to the reviewing user (or None if unauthenticated, which highlights the permission issue).
| API endpoint for submitting volunteer applications. | ||
| Currently contains: | ||
| POST /api/volunteer-applications/ |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring is incomplete and doesn't accurately describe the full functionality of this ViewSet. It only mentions the POST endpoint, but the class also implements:
- GET /api/volunteer-applications/ (admin-only list)
- GET /api/volunteer-applications/{id}/ (retrieve)
- PATCH /api/volunteer-applications/{id}/ (approve/reject)
Update the docstring to document all available endpoints and their access restrictions.
| API endpoint for submitting volunteer applications. | |
| Currently contains: | |
| POST /api/volunteer-applications/ | |
| API endpoint for managing volunteer applications. | |
| Endpoints: | |
| - POST /api/volunteer-applications/ | |
| Submit a new volunteer application. (Open to all) | |
| - GET /api/volunteer-applications/ | |
| List all volunteer applications. (Admin only) | |
| - GET /api/volunteer-applications/{id}/ | |
| Retrieve a specific volunteer application. (Admin only) | |
| - PATCH /api/volunteer-applications/{id}/ | |
| Approve or reject a volunteer application. (Admin only) | |
| Access: | |
| - POST: Any user (no authentication required) | |
| - GET (list/retrieve), PATCH: Admin users only (role == "ADMIN") |
| 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) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only the list() method has admin-only protection, but other methods like retrieve(), update(), partial_update(), and destroy() are not protected. This means:
- Anyone can GET individual applications (potentially exposing PII)
- Anyone can update/delete applications (due to AllowAny permissions)
Override these methods or use get_permissions() to ensure proper access control for all actions. Typically, only admins should be able to view individual applications and update their status.
| application.refresh_from_db() | ||
| assert application.status == "REJECTED" | ||
| assert application.reviewed_at is not None | ||
|
|
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the approval test, this test should also verify that reviewed_by is set when an application is rejected. The _handle_review_metadata method sets both reviewed_at and reviewed_by for any status change to APPROVED or REJECTED.
Add an assertion to verify application.reviewed_by is properly set.
| assert user.check_password("testpass123") | ||
|
|
||
|
|
||
| @pytest.mark.django_db |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for important edge cases:
- Approving an application when a User with that email already exists (should not create duplicate user)
- Multiple status changes on the same application (ensure
reviewed_atandreviewed_byare only set on first review) - Changing status from PENDING to PENDING (should not trigger review metadata or user creation)
Add tests for these scenarios to ensure the logic in _handle_review_metadata and _handle_volunteer_user_creation handles edge cases correctly.
| @@ -1,6 +1,10 @@ | |||
| import pytest | |||
| from django.test import Client | |||
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import of 'reverse' is not used.
qiuethan
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please resolve some of the copilot issues first, then re-request review :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Summary
Implements the volunteer application backend flow:
PENDING).Related Issues
Changes
status="PENDING"on create.reviewed_atandreviewed_byas read-only fields.POST /api/volunteer-applications/for public submission.GET /api/volunteer-applications/now restricted using a placeholder admin check:role == "ADMIN".PATCH /api/volunteer-applications/:id/for approving or rejecting applications.reviewed_atandreviewed_byon first review.APPROVED, creates a [User] with roleVOLUNTEERand a temporary password (invite/email flow to be implemented later).@transaction.atomicso status change, review metadata, and user creation are applied atomically.volunteer-applications→ [VolunteerApplicationAPIView] (basenamevolunteer-application).APPROVED:reviewed_at.role="VOLUNTEER"for the application email.REJECTED:reviewed_at.How to Test
Run the pytest:
cd backend pytest users/tests.pyChecklist