Skip to content

Commit 06a7c69

Browse files
authored
secondary application form backend (#64)
## Notion ticket link [Secondary Application Form](https://www.notion.so/uwblueprintexecs/Secondary-Application-Form-27210f3fb1dc80a7adcce3170891c182?source=copy_link) <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> ## Implementation description * resolved merge conflicts from: https://github.com/uwblueprint/llsc/pull/41/files * i think this is correct... <!-- What should the reviewer do to verify your changes? Describe expected results and include screenshots when appropriate --> ## Steps to test 1. <!-- Draw attention to the substantial parts of your PR or anything you'd like a second opinion on --> ## What should reviewers focus on? * ## Checklist - [ ] My PR name is descriptive and in imperative tense - [ ] My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits - [ ] I have run the appropriate linter(s) - [ ] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR
1 parent 10eb1f9 commit 06a7c69

File tree

13 files changed

+976
-1
lines changed

13 files changed

+976
-1
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Interface for volunteer data service operations.
3+
Defines the contract for volunteer data CRUD operations.
4+
"""
5+
6+
from abc import ABC, abstractmethod
7+
from typing import List
8+
9+
from app.schemas.volunteer_data import (
10+
VolunteerDataCreateRequest,
11+
VolunteerDataResponse,
12+
VolunteerDataUpdateRequest,
13+
)
14+
15+
16+
class IVolunteerDataService(ABC):
17+
"""
18+
Interface for volunteer data service operations
19+
"""
20+
21+
@abstractmethod
22+
async def create_volunteer_data(self, volunteer_data: VolunteerDataCreateRequest) -> VolunteerDataResponse:
23+
"""Create new volunteer data entry"""
24+
pass
25+
26+
@abstractmethod
27+
async def get_volunteer_data_by_id(self, volunteer_data_id: str) -> VolunteerDataResponse:
28+
"""Get volunteer data by ID"""
29+
pass
30+
31+
@abstractmethod
32+
async def get_volunteer_data_by_user_id(self, user_id: str) -> VolunteerDataResponse:
33+
"""Get volunteer data by user ID"""
34+
pass
35+
36+
@abstractmethod
37+
async def get_all_volunteer_data(self) -> List[VolunteerDataResponse]:
38+
"""Get all volunteer data entries"""
39+
pass
40+
41+
@abstractmethod
42+
async def update_volunteer_data_by_id(
43+
self, volunteer_data_id: str, volunteer_data_update: VolunteerDataUpdateRequest
44+
) -> VolunteerDataResponse:
45+
"""Update volunteer data by ID"""
46+
pass
47+
48+
@abstractmethod
49+
async def delete_volunteer_data_by_id(self, volunteer_data_id: str) -> None:
50+
"""Delete volunteer data by ID"""
51+
pass

backend/app/models/User.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ class User(Base):
4949
participant_matches = relationship("Match", back_populates="participant", foreign_keys=[Match.participant_id])
5050

5151
volunteer_matches = relationship("Match", back_populates="volunteer", foreign_keys=[Match.volunteer_id])
52+
53+
volunteer_data = relationship("VolunteerData", back_populates="user", uselist=False)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import uuid
2+
from datetime import datetime
3+
4+
from sqlalchemy import Column, DateTime, ForeignKey, Text
5+
from sqlalchemy.dialects.postgresql import UUID
6+
from sqlalchemy.orm import relationship
7+
8+
from .Base import Base
9+
10+
11+
class VolunteerData(Base):
12+
__tablename__ = "volunteer_data"
13+
14+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
15+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
16+
experience = Column(Text, nullable=True)
17+
references_json = Column(Text, nullable=True)
18+
additional_comments = Column(Text, nullable=True)
19+
submitted_at = Column(DateTime, default=datetime.utcnow, nullable=False)
20+
21+
user = relationship("User", back_populates="volunteer_data")

backend/app/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .Treatment import Treatment
2525
from .User import FormStatus, User
2626
from .UserData import UserData
27+
from .VolunteerData import VolunteerData
2728

2829
# Used to avoid import errors for the models
2930
__all__ = [
@@ -48,6 +49,7 @@
4849
"TaskType",
4950
"TaskPriority",
5051
"TaskStatus",
52+
"VolunteerData",
5153
]
5254

5355
log = logging.getLogger(LOGGER_NAME("models"))
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
3+
from app.middleware.auth import has_roles
4+
from app.schemas.user import UserRole
5+
from app.schemas.volunteer_data import (
6+
VolunteerDataCreateRequest,
7+
VolunteerDataListResponse,
8+
VolunteerDataPublicSubmission,
9+
VolunteerDataResponse,
10+
VolunteerDataUpdateRequest,
11+
)
12+
from app.services.implementations.volunteer_data_service import VolunteerDataService
13+
from app.utilities.service_utils import get_volunteer_data_service
14+
15+
router = APIRouter(
16+
prefix="/volunteer-data",
17+
tags=["volunteer-data"],
18+
)
19+
20+
21+
# Public endpoint - anyone can submit volunteer data
22+
@router.post("/submit", response_model=VolunteerDataResponse)
23+
async def submit_volunteer_data(
24+
volunteer_data: VolunteerDataPublicSubmission,
25+
volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service),
26+
):
27+
"""Public endpoint for volunteers to submit their application data"""
28+
try:
29+
create_request = VolunteerDataCreateRequest(
30+
user_id=None,
31+
experience=volunteer_data.experience,
32+
references_json=volunteer_data.references_json,
33+
additional_comments=volunteer_data.additional_comments,
34+
)
35+
return await volunteer_data_service.create_volunteer_data(create_request)
36+
except HTTPException as http_ex:
37+
raise http_ex
38+
except Exception as e:
39+
raise HTTPException(status_code=500, detail=str(e))
40+
41+
42+
# Admin only - create volunteer data
43+
@router.post("/", response_model=VolunteerDataResponse)
44+
async def create_volunteer_data(
45+
volunteer_data: VolunteerDataCreateRequest,
46+
volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service),
47+
authorized: bool = has_roles([UserRole.ADMIN]),
48+
):
49+
try:
50+
return await volunteer_data_service.create_volunteer_data(volunteer_data)
51+
except HTTPException as http_ex:
52+
raise http_ex
53+
except Exception as e:
54+
raise HTTPException(status_code=500, detail=str(e))
55+
56+
57+
# Admin only - get all volunteer data
58+
@router.get("/", response_model=VolunteerDataListResponse)
59+
async def get_all_volunteer_data(
60+
volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service),
61+
authorized: bool = has_roles([UserRole.ADMIN]),
62+
):
63+
try:
64+
volunteer_data_list = await volunteer_data_service.get_all_volunteer_data()
65+
return VolunteerDataListResponse(volunteer_data=volunteer_data_list, total=len(volunteer_data_list))
66+
except HTTPException as http_ex:
67+
raise http_ex
68+
except Exception as e:
69+
raise HTTPException(status_code=500, detail=str(e))
70+
71+
72+
# Admin only - get volunteer data by ID
73+
@router.get("/{volunteer_data_id}", response_model=VolunteerDataResponse)
74+
async def get_volunteer_data(
75+
volunteer_data_id: str,
76+
volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service),
77+
authorized: bool = has_roles([UserRole.ADMIN]),
78+
):
79+
try:
80+
return await volunteer_data_service.get_volunteer_data_by_id(volunteer_data_id)
81+
except HTTPException as http_ex:
82+
raise http_ex
83+
except Exception as e:
84+
raise HTTPException(status_code=500, detail=str(e))
85+
86+
87+
# Admin only - get volunteer data by user ID
88+
@router.get("/user/{user_id}", response_model=VolunteerDataResponse)
89+
async def get_volunteer_data_by_user(
90+
user_id: str,
91+
volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service),
92+
authorized: bool = has_roles([UserRole.ADMIN]),
93+
):
94+
try:
95+
return await volunteer_data_service.get_volunteer_data_by_user_id(user_id)
96+
except HTTPException as http_ex:
97+
raise http_ex
98+
except Exception as e:
99+
raise HTTPException(status_code=500, detail=str(e))
100+
101+
102+
# Admin only - update volunteer data
103+
@router.put("/{volunteer_data_id}", response_model=VolunteerDataResponse)
104+
async def update_volunteer_data(
105+
volunteer_data_id: str,
106+
volunteer_data_update: VolunteerDataUpdateRequest,
107+
volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service),
108+
authorized: bool = has_roles([UserRole.ADMIN]),
109+
):
110+
try:
111+
return await volunteer_data_service.update_volunteer_data_by_id(volunteer_data_id, volunteer_data_update)
112+
except HTTPException as http_ex:
113+
raise http_ex
114+
except Exception as e:
115+
raise HTTPException(status_code=500, detail=str(e))
116+
117+
118+
# Admin only - delete volunteer data
119+
@router.delete("/{volunteer_data_id}")
120+
async def delete_volunteer_data(
121+
volunteer_data_id: str,
122+
volunteer_data_service: VolunteerDataService = Depends(get_volunteer_data_service),
123+
authorized: bool = has_roles([UserRole.ADMIN]),
124+
):
125+
try:
126+
await volunteer_data_service.delete_volunteer_data_by_id(volunteer_data_id)
127+
return {"message": "Volunteer data deleted successfully"}
128+
except HTTPException as http_ex:
129+
raise http_ex
130+
except Exception as e:
131+
raise HTTPException(status_code=500, detail=str(e))
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Pydantic schemas for volunteer data validation and serialization.
3+
Handles volunteer data CRUD and response models for the API.
4+
"""
5+
6+
from datetime import datetime
7+
from typing import List, Optional
8+
from uuid import UUID
9+
10+
from pydantic import BaseModel, ConfigDict, Field
11+
12+
13+
class VolunteerDataBase(BaseModel):
14+
"""
15+
Base schema for volunteer data model with common attributes.
16+
"""
17+
18+
experience: Optional[str] = Field(None, description="Volunteer experience description")
19+
references_json: Optional[str] = Field(None, description="JSON string containing references")
20+
additional_comments: Optional[str] = Field(None, description="Additional comments about volunteering")
21+
22+
23+
class VolunteerDataCreateRequest(VolunteerDataBase):
24+
"""
25+
Request schema for creating volunteer data
26+
"""
27+
28+
user_id: Optional[UUID] = Field(
29+
None, description="User ID this volunteer data belongs to (optional for public submissions)"
30+
)
31+
32+
33+
class VolunteerDataPublicSubmission(VolunteerDataBase):
34+
"""
35+
Request schema for public volunteer data submissions (no user_id required)
36+
"""
37+
38+
pass
39+
40+
41+
class VolunteerDataUpdateRequest(BaseModel):
42+
"""
43+
Request schema for updating volunteer data, all fields optional
44+
"""
45+
46+
experience: Optional[str] = Field(None, description="Volunteer experience description")
47+
references_json: Optional[str] = Field(None, description="JSON string containing references")
48+
additional_comments: Optional[str] = Field(None, description="Additional comments about volunteering")
49+
50+
51+
class VolunteerDataResponse(BaseModel):
52+
"""
53+
Response schema for volunteer data
54+
"""
55+
56+
id: UUID
57+
user_id: Optional[UUID]
58+
experience: Optional[str]
59+
references_json: Optional[str]
60+
additional_comments: Optional[str]
61+
submitted_at: datetime
62+
63+
model_config = ConfigDict(from_attributes=True)
64+
65+
66+
class VolunteerDataListResponse(BaseModel):
67+
"""
68+
Response schema for listing volunteer data
69+
"""
70+
71+
volunteer_data: List[VolunteerDataResponse]
72+
total: int

backend/app/server.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,20 @@
88

99
from . import models
1010
from .middleware.auth_middleware import AuthMiddleware
11-
from .routes import auth, availability, intake, match, matching, ranking, send_email, suggested_times, task, test, user
11+
from .routes import (
12+
auth,
13+
availability,
14+
intake,
15+
match,
16+
matching,
17+
ranking,
18+
send_email,
19+
suggested_times,
20+
task,
21+
test,
22+
user,
23+
volunteer_data,
24+
)
1225
from .utilities.constants import LOGGER_NAME
1326
from .utilities.firebase_init import initialize_firebase
1427
from .utilities.ses.ses_init import ensure_ses_templates
@@ -71,6 +84,7 @@ async def lifespan(_: FastAPI):
7184
app.include_router(matching.router)
7285
app.include_router(intake.router)
7386
app.include_router(ranking.router)
87+
app.include_router(volunteer_data.router)
7488
app.include_router(send_email.router)
7589
app.include_router(task.router)
7690
app.include_router(test.router)

0 commit comments

Comments
 (0)