From 2eaccfdf9610ba573c40202e8b44824be61740d4 Mon Sep 17 00:00:00 2001 From: Ian Dai Date: Sun, 8 Dec 2024 17:49:39 -0800 Subject: [PATCH 1/4] Added generalized flow for applications --- apps/api/src/routers/user.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/api/src/routers/user.py b/apps/api/src/routers/user.py index b0e95afc..552bccb9 100644 --- a/apps/api/src/routers/user.py +++ b/apps/api/src/routers/user.py @@ -9,7 +9,10 @@ from auth import user_identity from auth.authorization import require_accepted_applicant from auth.user_identity import User, require_user_identity, use_user_identity -from models.ApplicationData import ProcessedApplicationData, RawApplicationData +from models.ApplicationData import ( + ProcessedApplicationData, + RawApplicationData, +) from models.user_record import Applicant, BareApplicant, Role, Status from services import docusign_handler, mongodb_handler from services.docusign_handler import WebhookPayload @@ -67,14 +70,11 @@ async def me( return IdentityResponse(uid=user.uid, **user_record) -@router.post("/apply", status_code=status.HTTP_201_CREATED) -async def apply( - user: Annotated[User, Depends(require_user_identity)], - # media type should be automatically detected but seems like a bug as of now - raw_application_data: Annotated[ - RawApplicationData, Form(media_type="multipart/form-data") - ], -) -> str: +async def apply_flow( + user: User, + raw_application_data: Union[RawApplicationData], + processed_application_type: Union[RawApplicationData], +): if raw_application_data.application_type not in Role.__members__: raise HTTPException( status.HTTP_422_UNPROCESSABLE_ENTITY, @@ -131,7 +131,7 @@ async def apply( else: resume_url = None - processed_application_data = ProcessedApplicationData( + processed_application_data = processed_application_type( **raw_app_data_dump, resume_url=resume_url, submission_time=now, @@ -174,6 +174,20 @@ async def apply( ) +@router.post("/apply", status_code=status.HTTP_201_CREATED) +async def apply( + user: Annotated[User, Depends(require_user_identity)], + # media type should be automatically detected but seems like a bug as of now + raw_application_data: Annotated[ + RawApplicationData, + Form(media_type="multipart/form-data"), + ], +) -> str: + return await apply_flow( + user, raw_application_data, processed_application_type=ProcessedApplicationData + ) + + @router.get("/waiver") async def request_waiver( user: Annotated[tuple[User, BareApplicant], Depends(require_accepted_applicant)] From b7edc8bee59c250297b0fa2798885fd6f9ef6336 Mon Sep 17 00:00:00 2001 From: Ian Dai Date: Sun, 8 Dec 2024 18:07:02 -0800 Subject: [PATCH 2/4] Updated typing --- apps/api/src/routers/user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/src/routers/user.py b/apps/api/src/routers/user.py index 552bccb9..b15eccb7 100644 --- a/apps/api/src/routers/user.py +++ b/apps/api/src/routers/user.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone from logging import getLogger -from typing import Annotated, Union +from typing import Annotated, Type, Union from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, status from fastapi.responses import RedirectResponse @@ -73,8 +73,8 @@ async def me( async def apply_flow( user: User, raw_application_data: Union[RawApplicationData], - processed_application_type: Union[RawApplicationData], -): + processed_application_type: Union[Type[ProcessedApplicationData]], +) -> str: if raw_application_data.application_type not in Role.__members__: raise HTTPException( status.HTTP_422_UNPROCESSABLE_ENTITY, From 96e08fa0ea7d903bcc37d530e31bfec1f085f051 Mon Sep 17 00:00:00 2001 From: Albert Wang Date: Sun, 8 Dec 2024 22:07:29 -0800 Subject: [PATCH 3/4] add mentor backend route and finish hacker frontend app --- apps/api/src/models/ApplicationData.py | 50 ++++++- apps/api/src/models/user_record.py | 8 +- apps/api/src/routers/user.py | 63 +++++--- apps/api/tests/test_user_apply.py | 6 +- apps/api/tests/test_user_mentor_apply.py | 141 ++++++++++++++++++ .../apply/Form/HackerBasicInformation.tsx | 2 +- .../src/app/(main)/apply/Form/HackerForm.tsx | 2 +- .../(main)/apply/Form/ProfileInformation.tsx | 21 ++- .../src/app/(main)/mentor/Form/MentorForm.tsx | 2 +- .../lib/components/forms/shared/BaseForm.tsx | 10 +- apps/site/tsconfig.json | 2 +- 11 files changed, 268 insertions(+), 39 deletions(-) create mode 100644 apps/api/tests/test_user_mentor_apply.py diff --git a/apps/api/src/models/ApplicationData.py b/apps/api/src/models/ApplicationData.py index 95f94995..80913cd1 100644 --- a/apps/api/src/models/ApplicationData.py +++ b/apps/api/src/models/ApplicationData.py @@ -45,11 +45,41 @@ class BaseApplicationData(BaseModel): is_first_hackathon: bool linkedin: NullableHttpUrl = None portfolio: NullableHttpUrl = None - frq_collaboration: Union[str, None] = Field(None, max_length=2048) - frq_dream_job: str = Field(max_length=2048) + frq_change: Union[str, None] = Field(None, max_length=2048) + frq_video_game: str = Field(max_length=2048) -class RawApplicationData(BaseApplicationData): +class BaseMentorApplicationData(BaseModel): + model_config = ConfigDict(str_strip_whitespace=True, str_max_length=254) + + experienced_technologies: str + pronouns: str + + school: str + major: str + education_level: str + is_18_older: str + git_experience: str + github: NullableHttpUrl = None + portfolio: NullableHttpUrl = None + linkedin: NullableHttpUrl = None + mentor_prev_experience_saq1: Union[str, None] = Field(None, max_length=2048) + mentor_interest_saq2: Union[str, None] = Field(None, max_length=2048) + mentor_team_help_saq3: Union[str, None] = Field(None, max_length=2048) + mentor_team_help_saq4: Union[str, None] = Field(None, max_length=2048) + other_questions: Union[str, None] = Field(None, max_length=2048) + + +class RawHackerApplicationData(BaseApplicationData): + """Expected to be sent by the form on the site.""" + + first_name: str + last_name: str + resume: Union[UploadFile, None] = None + application_type: str + + +class RawMentorApplicationData(BaseMentorApplicationData): """Expected to be sent by the form on the site.""" first_name: str @@ -58,7 +88,19 @@ class RawApplicationData(BaseApplicationData): application_type: str -class ProcessedApplicationData(BaseApplicationData): +class ProcessedHackerApplicationData(BaseApplicationData): + resume_url: Union[HttpUrl, None] = None + submission_time: datetime + reviews: list[Review] = [] + + @field_serializer("linkedin", "portfolio", "resume_url") + def url2str(self, val: Union[HttpUrl, None]) -> Union[str, None]: + if val is not None: + return str(val) + return val + + +class ProcessedMentorApplicationData(BaseMentorApplicationData): resume_url: Union[HttpUrl, None] = None submission_time: datetime reviews: list[Review] = [] diff --git a/apps/api/src/models/user_record.py b/apps/api/src/models/user_record.py index a25dd24b..1d57ae62 100644 --- a/apps/api/src/models/user_record.py +++ b/apps/api/src/models/user_record.py @@ -4,7 +4,11 @@ from pydantic import AfterValidator, Field from typing_extensions import TypeAlias -from models.ApplicationData import Decision, ProcessedApplicationData +from models.ApplicationData import ( + Decision, + ProcessedHackerApplicationData, + ProcessedMentorApplicationData, +) from services.mongodb_handler import BaseRecord @@ -72,4 +76,4 @@ class Applicant(BareApplicant): # Note validators not run on default values roles: RoleWithApplicant = (Role.APPLICANT,) - application_data: ProcessedApplicationData + application_data: Union[ProcessedHackerApplicationData, ProcessedMentorApplicationData] diff --git a/apps/api/src/routers/user.py b/apps/api/src/routers/user.py index b15eccb7..4da7617b 100644 --- a/apps/api/src/routers/user.py +++ b/apps/api/src/routers/user.py @@ -1,17 +1,19 @@ from datetime import datetime, timezone from logging import getLogger -from typing import Annotated, Type, Union +from typing import Annotated, Union from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, status from fastapi.responses import RedirectResponse -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, TypeAdapter from auth import user_identity from auth.authorization import require_accepted_applicant from auth.user_identity import User, require_user_identity, use_user_identity from models.ApplicationData import ( - ProcessedApplicationData, - RawApplicationData, + ProcessedHackerApplicationData, + RawHackerApplicationData, + RawMentorApplicationData, + ProcessedMentorApplicationData, ) from models.user_record import Applicant, BareApplicant, Role, Status from services import docusign_handler, mongodb_handler @@ -72,9 +74,9 @@ async def me( async def apply_flow( user: User, - raw_application_data: Union[RawApplicationData], - processed_application_type: Union[Type[ProcessedApplicationData]], + raw_application_data: Union[RawHackerApplicationData, RawMentorApplicationData], ) -> str: + print("apply_flow called") if raw_application_data.application_type not in Role.__members__: raise HTTPException( status.HTTP_422_UNPROCESSABLE_ENTITY, @@ -103,7 +105,7 @@ async def apply_flow( raw_app_data_dump = raw_application_data.model_dump() for field in ["pronouns", "ethnicity", "school", "major"]: - if raw_app_data_dump[field] == "other": + if field in raw_app_data_dump and raw_app_data_dump[field] == "other": raise HTTPException( status.HTTP_422_UNPROCESSABLE_ENTITY, "Please enable JavaScript on your browser.", @@ -131,10 +133,17 @@ async def apply_flow( else: resume_url = None - processed_application_data = processed_application_type( - **raw_app_data_dump, - resume_url=resume_url, - submission_time=now, + ProcessedApplicationData: TypeAdapter[ + Union[ProcessedHackerApplicationData, ProcessedMentorApplicationData] + ] = TypeAdapter( + Union[ProcessedHackerApplicationData, ProcessedMentorApplicationData] + ) + processed_application_data = ProcessedApplicationData.validate_python( + { + **raw_app_data_dump, + "resume_url": resume_url, + "submission_time": now, + } ) applicant = Applicant( uid=user.uid, @@ -157,13 +166,13 @@ async def apply_flow( log.error("Could not insert applicant %s to MongoDB.", user.uid) raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) - try: - await email_handler.send_application_confirmation_email( - user.email, applicant, Role[raw_application_data.application_type] - ) - except RuntimeError: - log.error("Could not send confirmation email with SendGrid to %s.", user.uid) - raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) + # try: + # await email_handler.send_application_confirmation_email( + # user.email, applicant, Role[raw_application_data.application_type] + # ) + # except RuntimeError: + # log.error("Could not send confirmation email with SendGrid to %s.", user.uid) + # raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) # TODO: handle inconsistent results if one service fails @@ -179,13 +188,23 @@ async def apply( user: Annotated[User, Depends(require_user_identity)], # media type should be automatically detected but seems like a bug as of now raw_application_data: Annotated[ - RawApplicationData, + RawHackerApplicationData, Form(media_type="multipart/form-data"), ], ) -> str: - return await apply_flow( - user, raw_application_data, processed_application_type=ProcessedApplicationData - ) + return await apply_flow(user, raw_application_data) + + +@router.post("/mentor", status_code=status.HTTP_201_CREATED) +async def mentor( + user: Annotated[User, Depends(require_user_identity)], + # media type should be automatically detected but seems like a bug as of now + raw_application_data: Annotated[ + RawMentorApplicationData, + Form(media_type="multipart/form-data"), + ], +) -> str: + return await apply_flow(user, raw_application_data) @router.get("/waiver") diff --git a/apps/api/tests/test_user_apply.py b/apps/api/tests/test_user_apply.py index 7a70bd39..f68ce74c 100644 --- a/apps/api/tests/test_user_apply.py +++ b/apps/api/tests/test_user_apply.py @@ -7,7 +7,7 @@ from pydantic import HttpUrl from auth.user_identity import NativeUser, UserTestClient -from models.ApplicationData import ProcessedApplicationData +from models.ApplicationData import ProcessedHackerApplicationData from models.user_record import Applicant, Status, Role from routers import user from services.mongodb_handler import Collection @@ -60,7 +60,7 @@ SAMPLE_SUBMISSION_TIME = datetime(2024, 1, 12, 8, 1, 21, tzinfo=timezone.utc) SAMPLE_VERDICT_TIME = None -EXPECTED_APPLICATION_DATA = ProcessedApplicationData( +EXPECTED_APPLICATION_DATA = ProcessedHackerApplicationData( **SAMPLE_APPLICATION, # type: ignore[arg-type] resume_url=SAMPLE_RESUME_URL, submission_time=SAMPLE_SUBMISSION_TIME, @@ -68,7 +68,7 @@ ) assert EXPECTED_APPLICATION_DATA.linkedin is None -EXPECTED_APPLICATION_DATA_WITHOUT_RESUME = ProcessedApplicationData( +EXPECTED_APPLICATION_DATA_WITHOUT_RESUME = ProcessedHackerApplicationData( **SAMPLE_APPLICATION, # type: ignore[arg-type] resume_url=None, submission_time=SAMPLE_SUBMISSION_TIME, diff --git a/apps/api/tests/test_user_mentor_apply.py b/apps/api/tests/test_user_mentor_apply.py new file mode 100644 index 00000000..4fa13727 --- /dev/null +++ b/apps/api/tests/test_user_mentor_apply.py @@ -0,0 +1,141 @@ +from datetime import datetime, timezone +from unittest.mock import AsyncMock, Mock, patch + +from fastapi import FastAPI +from pydantic import HttpUrl + +from auth.user_identity import NativeUser, UserTestClient +from models.ApplicationData import ( + ProcessedMentorApplicationData, +) +from models.user_record import Applicant, Status, Role +from routers import user +from services.mongodb_handler import Collection +from utils import resume_handler + +# Tests will break again next year, tech should notice and fix :P +TEST_DEADLINE = datetime(2025, 10, 1, 8, 0, 0, tzinfo=timezone.utc) +user.DEADLINE = TEST_DEADLINE + +USER_EMAIL = "pkfire@uci.edu" +USER_PKFIRE = NativeUser( + ucinetid="pkfire", + display_name="pkfire", + email=USER_EMAIL, + affiliations=["pkfire"], +) + +SAMPLE_APPLICATION = { + "first_name": "pk", + "last_name": "fire", + "experienced_technologies": "", + "pronouns": "", + "is_18_older": "true", + "school": "UC Irvine", + "education_level": "Fifth+ Year Undergraduate", + "major": "Computer Science", + "git_experience": "2", + "github": "https://github.com", + "portfolio": "", + "linkedin": "", + "mentor_prev_experience_saq1": "", + "mentor_interest_saq2": "", + "mentor_team_help_saq3": "", + "mentor_team_help_saq4": "", + "other_questions": "", + "application_type": "MENTOR", +} + + +SAMPLE_RESUME = ("my-resume.pdf", b"resume", "application/pdf") +SAMPLE_FILES = {"resume": SAMPLE_RESUME} +BAD_RESUME = ("bad-resume.doc", b"resume", "application/msword") +LARGE_RESUME = ("large-resume.pdf", b"resume" * 100_000, "application/pdf") +# The browser will send an empty file if not selected +EMPTY_RESUME = ( + "", + b"", + "application/octet-stream", + {"content-disposition": 'form-data; name="resume"; filename=""'}, +) + +EXPECTED_RESUME_UPLOAD = ("pk-fire-69f2afc2.pdf", b"resume", "application/pdf") +SAMPLE_RESUME_URL = HttpUrl("https://drive.google.com/file/d/...") +SAMPLE_SUBMISSION_TIME = datetime(2024, 1, 12, 8, 1, 21, tzinfo=timezone.utc) +SAMPLE_VERDICT_TIME = None + +EXPECTED_APPLICATION_DATA = ProcessedMentorApplicationData( + **SAMPLE_APPLICATION, # type: ignore[arg-type] + resume_url=SAMPLE_RESUME_URL, + submission_time=SAMPLE_SUBMISSION_TIME, + verdict_time=SAMPLE_VERDICT_TIME, +) +assert EXPECTED_APPLICATION_DATA.linkedin is None + +EXPECTED_APPLICATION_DATA_WITHOUT_RESUME = ProcessedMentorApplicationData( + **SAMPLE_APPLICATION, # type: ignore[arg-type] + resume_url=None, + submission_time=SAMPLE_SUBMISSION_TIME, + verdict_time=SAMPLE_VERDICT_TIME, +) + +EXPECTED_USER = Applicant( + uid="edu.uci.pkfire", + first_name="pk", + last_name="fire", + roles=(Role.APPLICANT, Role.MENTOR), + application_data=EXPECTED_APPLICATION_DATA, + status=Status.PENDING_REVIEW, +) + +EXPECTED_USER_WITHOUT_RESUME = Applicant( + uid="edu.uci.pkfire", + first_name="pk", + last_name="fire", + roles=(Role.APPLICANT, Role.HACKER), + status=Status.PENDING_REVIEW, + application_data=EXPECTED_APPLICATION_DATA_WITHOUT_RESUME, +) + +resume_handler.RESUMES_FOLDER_ID = "RESUMES_FOLDER_ID" + +app = FastAPI() +app.include_router(user.router) + +client = UserTestClient(USER_PKFIRE, app) + + +@patch("utils.email_handler.send_application_confirmation_email", autospec=True) +@patch("services.mongodb_handler.update_one", autospec=True) +@patch("routers.user._is_past_deadline", autospec=True) +@patch("routers.user.datetime", autospec=True) +@patch("services.gdrive_handler.upload_file", autospec=True) +@patch("services.mongodb_handler.retrieve_one", autospec=True) +def test_mentor_apply_successfully( + mock_mongodb_handler_retrieve_one: AsyncMock, + mock_gdrive_handler_upload_file: AsyncMock, + mock_datetime: Mock, + mock_is_past_deadline: Mock, + mock_mongodb_handler_update_one: AsyncMock, + mock_send_application_confirmation_email: AsyncMock, +) -> None: + """Test that a valid application is submitted properly.""" + mock_mongodb_handler_retrieve_one.return_value = None + mock_gdrive_handler_upload_file.return_value = SAMPLE_RESUME_URL + mock_datetime.now.return_value = SAMPLE_SUBMISSION_TIME + mock_is_past_deadline.return_value = False + res = client.post("/mentor", data=SAMPLE_APPLICATION, files=SAMPLE_FILES) + + mock_gdrive_handler_upload_file.assert_awaited_once_with( + resume_handler.RESUMES_FOLDER_ID, *EXPECTED_RESUME_UPLOAD + ) + mock_mongodb_handler_update_one.assert_awaited_once_with( + Collection.USERS, + {"_id": EXPECTED_USER.uid}, + EXPECTED_USER.model_dump(), + upsert=True, + ) + mock_send_application_confirmation_email.assert_awaited_once_with( + USER_EMAIL, EXPECTED_USER, Role.MENTOR + ) + assert res.status_code == 201 diff --git a/apps/site/src/app/(main)/apply/Form/HackerBasicInformation.tsx b/apps/site/src/app/(main)/apply/Form/HackerBasicInformation.tsx index 60e9cc33..d6da3d9a 100644 --- a/apps/site/src/app/(main)/apply/Form/HackerBasicInformation.tsx +++ b/apps/site/src/app/(main)/apply/Form/HackerBasicInformation.tsx @@ -47,7 +47,7 @@ export default function BasicInformation() { placeholder="Anteater" /> +
diff --git a/apps/site/src/app/(main)/apply/Form/ProfileInformation.tsx b/apps/site/src/app/(main)/apply/Form/ProfileInformation.tsx index 720a966c..f641e1f6 100644 --- a/apps/site/src/app/(main)/apply/Form/ProfileInformation.tsx +++ b/apps/site/src/app/(main)/apply/Form/ProfileInformation.tsx @@ -1,5 +1,8 @@ +import Textfield from "@/lib/components/forms/Textfield"; import TextInput from "@/lib/components/forms/TextInput"; +const FRQ_MAX_LENGTH = 2000; + export default function ProfileInformation() { return (
@@ -24,9 +27,21 @@ export default function ProfileInformation() { placeholder="https://" />
- {/* - TODO Add SAQs here using Textfield - */} + + + ); } diff --git a/apps/site/src/app/(main)/mentor/Form/MentorForm.tsx b/apps/site/src/app/(main)/mentor/Form/MentorForm.tsx index 7cdbc924..984c46dc 100644 --- a/apps/site/src/app/(main)/mentor/Form/MentorForm.tsx +++ b/apps/site/src/app/(main)/mentor/Form/MentorForm.tsx @@ -14,7 +14,7 @@ import MultipleSelect from "@/lib/components/forms/MultipleSelect"; export default function MentorForm() { return ( - + diff --git a/apps/site/src/lib/components/forms/shared/BaseForm.tsx b/apps/site/src/lib/components/forms/shared/BaseForm.tsx index d492417c..f061ff39 100644 --- a/apps/site/src/lib/components/forms/shared/BaseForm.tsx +++ b/apps/site/src/lib/components/forms/shared/BaseForm.tsx @@ -10,7 +10,12 @@ import hasDeadlinePassed from "@/lib/utils/hasDeadlinePassed"; const APPLY_PATH = "/api/user/apply"; const FIELDS_WITH_OTHER = ["pronouns", "ethnicity", "school", "major"]; -export default function BaseForm({ children }: PropsWithChildren) { +export default function BaseForm({ + applicationType, + children, +}: { + applicationType: string; +} & PropsWithChildren) { const [submitting, setSubmitting] = useState(false); const [sessionExpired, setSessionExpired] = useState(false); @@ -42,6 +47,9 @@ export default function BaseForm({ children }: PropsWithChildren) { } } + // attach application type to formData + formData.append("application_type", applicationType); + try { const res = await axios.post(APPLY_PATH, formData); if (res.status === 201) { diff --git a/apps/site/tsconfig.json b/apps/site/tsconfig.json index 1bf25398..5f00b1d3 100644 --- a/apps/site/tsconfig.json +++ b/apps/site/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, From 609aef2b223f1837d336ee32cc8dff486542936f Mon Sep 17 00:00:00 2001 From: Albert Wang Date: Sun, 8 Dec 2024 22:15:28 -0800 Subject: [PATCH 4/4] fix build issues --- apps/api/src/models/user_record.py | 4 +++- apps/api/src/routers/user.py | 14 +++++++------- apps/api/tests/test_user_apply.py | 6 +++--- .../src/app/(main)/volunteer/VolunteerForm.tsx | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/api/src/models/user_record.py b/apps/api/src/models/user_record.py index 1d57ae62..8d55335b 100644 --- a/apps/api/src/models/user_record.py +++ b/apps/api/src/models/user_record.py @@ -76,4 +76,6 @@ class Applicant(BareApplicant): # Note validators not run on default values roles: RoleWithApplicant = (Role.APPLICANT,) - application_data: Union[ProcessedHackerApplicationData, ProcessedMentorApplicationData] + application_data: Union[ + ProcessedHackerApplicationData, ProcessedMentorApplicationData + ] diff --git a/apps/api/src/routers/user.py b/apps/api/src/routers/user.py index 4da7617b..eaceca09 100644 --- a/apps/api/src/routers/user.py +++ b/apps/api/src/routers/user.py @@ -166,13 +166,13 @@ async def apply_flow( log.error("Could not insert applicant %s to MongoDB.", user.uid) raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) - # try: - # await email_handler.send_application_confirmation_email( - # user.email, applicant, Role[raw_application_data.application_type] - # ) - # except RuntimeError: - # log.error("Could not send confirmation email with SendGrid to %s.", user.uid) - # raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) + try: + await email_handler.send_application_confirmation_email( + user.email, applicant, Role[raw_application_data.application_type] + ) + except RuntimeError: + log.error("Could not send confirmation email with SendGrid to %s.", user.uid) + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) # TODO: handle inconsistent results if one service fails diff --git a/apps/api/tests/test_user_apply.py b/apps/api/tests/test_user_apply.py index f68ce74c..4952d9f1 100644 --- a/apps/api/tests/test_user_apply.py +++ b/apps/api/tests/test_user_apply.py @@ -37,8 +37,8 @@ "is_first_hackathon": "false", "linkedin": "", "portfolio": "https://github.com", - "frq_collaboration": "I am pkfire", - "frq_dream_job": "I am pkfire", + "frq_change": "I am pkfire", + "frq_video_game": "I am pkfire", "application_type": "HACKER", } @@ -302,7 +302,7 @@ def test_application_data_is_bson_encodable() -> None: data = EXPECTED_APPLICATION_DATA.model_copy() data.linkedin = HttpUrl("https://linkedin.com") encoded = bson.encode(EXPECTED_APPLICATION_DATA.model_dump()) - assert len(encoded) == 376 + assert len(encoded) == 370 @patch("services.mongodb_handler.retrieve_one", autospec=True) diff --git a/apps/site/src/app/(main)/volunteer/VolunteerForm.tsx b/apps/site/src/app/(main)/volunteer/VolunteerForm.tsx index 38e01375..7e4cfa8f 100644 --- a/apps/site/src/app/(main)/volunteer/VolunteerForm.tsx +++ b/apps/site/src/app/(main)/volunteer/VolunteerForm.tsx @@ -9,7 +9,7 @@ import ExtraQuestions from "./components/ExtraQuestions"; export default function VolunteerForm() { return ( - +