Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/organizer-carousel
Browse files Browse the repository at this point in the history
  • Loading branch information
pandyrew committed Jan 18, 2025
2 parents 798d203 + 962a6fe commit 798d39e
Show file tree
Hide file tree
Showing 108 changed files with 3,789 additions and 915 deletions.
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dev:default": "./run-docker.sh",
"test": "pytest",
"lint": "flake8 src tests && mypy src tests",
"build": "mkdir -p lib && pip install --target lib -r requirements.txt",
"build": "mkdir -p lib && pip3 install --target lib -r requirements.txt",
"format:write": "black src tests",
"format:check": "black --check src tests"
},
Expand Down
11 changes: 9 additions & 2 deletions apps/api/src/admin/applicant_review_processor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from typing import Any
from typing import Any, Optional

from models.ApplicationData import Decision

scores_to_decisions: dict[Optional[int], Decision] = {
100: Decision.ACCEPTED,
-2: Decision.WAITLISTED,
0: Decision.REJECTED,
}


def include_hacker_app_fields(
applicant_record: dict[str, Any], accept_threshold: float, waitlist_threshold: float
Expand All @@ -16,7 +22,8 @@ def include_hacker_app_fields(
def include_review_decision(applicant_record: dict[str, Any]) -> None:
"""Sets the applicant's decision as the last submitted review decision or None."""
reviews = applicant_record["application_data"]["reviews"]
applicant_record["decision"] = reviews[-1][2] if reviews else None
score: Optional[int] = reviews[-1][2] if reviews else None
applicant_record["decision"] = scores_to_decisions.get(score)


def get_unique_reviewers(applicant_record: dict[str, Any]) -> set[str]:
Expand Down
91 changes: 89 additions & 2 deletions apps/api/src/admin/summary_handler.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from collections import Counter
from collections import Counter, defaultdict
from datetime import date, datetime
from typing import Iterable
from zoneinfo import ZoneInfo

from pydantic import BaseModel, TypeAdapter
from pydantic import BaseModel, TypeAdapter, ValidationError

from models.user_record import ApplicantStatus, Role
from services import mongodb_handler
from services.mongodb_handler import Collection

LOCAL_TIMEZONE = ZoneInfo("America/Los_Angeles")


class ApplicantSummaryRecord(BaseModel):
status: ApplicantStatus
Expand All @@ -21,3 +26,85 @@ async def applicant_summary() -> Counter[ApplicantStatus]:
applicants = TypeAdapter(list[ApplicantSummaryRecord]).validate_python(records)

return Counter(applicant.status for applicant in applicants)


class ApplicationSubmissionTime(BaseModel):
submission_time: datetime


class ApplicationSchoolAndTime(ApplicationSubmissionTime):
school: str


class ApplicantSchoolStats(BaseModel):
application_data: ApplicationSchoolAndTime


async def applications_by_school() -> dict[str, dict[date, int]]:
"""Get daily number of applications by school."""
records = await mongodb_handler.retrieve(
Collection.USERS,
{"roles": Role.APPLICANT},
["application_data.school", "application_data.submission_time"],
)

try:
applicant_stats_adapter = TypeAdapter(list[ApplicantSchoolStats])
applicants = applicant_stats_adapter.validate_python(records)
except ValidationError:
raise RuntimeError("Could not parse applicant data.")

grouped_applications: dict[str, dict[date, int]] = defaultdict(
lambda: defaultdict(int)
)

for applicant in applicants:
school = applicant.application_data.school
day = applicant.application_data.submission_time.astimezone(
LOCAL_TIMEZONE
).date()
grouped_applications[school][day] += 1

return grouped_applications


class ApplicantRoleStats(BaseModel):
roles: tuple[Role, ...]
application_data: ApplicationSubmissionTime


async def applications_by_role() -> dict[str, dict[date, int]]:
"""Get daily number of applications by role."""
records: list[dict[str, object]] = await mongodb_handler.retrieve(
Collection.USERS,
{"roles": Role.APPLICANT},
["roles", "application_data.submission_time"],
)

try:
applicant_stats_adapter = TypeAdapter(list[ApplicantRoleStats])
applicants = applicant_stats_adapter.validate_python(records)
except ValidationError:
raise RuntimeError("Could not parse applicant data.")

return {
role.value: _count_applications_by_day(
applicant.application_data
for applicant in applicants
if role in applicant.roles
)
for role in [Role.HACKER, Role.MENTOR, Role.VOLUNTEER]
}


def _count_applications_by_day(
application_data: Iterable[ApplicationSubmissionTime],
) -> dict[date, int]:
"""Group the applications by the date of submission."""
daily_applications = defaultdict[date, int](int)

for data in application_data:
day = data.submission_time.astimezone(LOCAL_TIMEZONE).date()
daily_applications[day] += 1

return daily_applications
3 changes: 2 additions & 1 deletion apps/api/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from fastapi import FastAPI

from routers import admin, guest, saml, user
from routers import admin, director, guest, saml, user

logging.basicConfig(level=logging.INFO)

Expand All @@ -23,6 +23,7 @@
app.include_router(guest.router, prefix="/guest", tags=["guest"])
app.include_router(user.router, prefix="/user", tags=["user"])
app.include_router(admin.router, prefix="/admin", tags=["admin"])
app.include_router(director.router, prefix="/director", tags=["director"])


@app.get("/")
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/auth/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def require_accepted_applicant(
Decision.ACCEPTED,
Status.WAIVER_SIGNED,
Status.CONFIRMED,
Status.ATTENDING,
):
raise HTTPException(status.HTTP_403_FORBIDDEN, "User was not accepted.")

Expand Down
1 change: 0 additions & 1 deletion apps/api/src/models/ApplicationData.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ class BaseVolunteerApplicationData(BaseModel):
school: str
education_level: str
major: str
applied_before: bool
frq_volunteer: str = Field(max_length=2048)
frq_utensil: str = Field(max_length=2048)
allergies: Union[str, None] = Field(None, max_length=2048)
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/models/user_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ class Role(str, Enum):
DIRECTOR = "Director"
HACKER = "Hacker"
MENTOR = "Mentor"
REVIEWER = "Reviewer"
REVIEWER = (
"Reviewer" # leaving this role in for now, but might be removed down the line
)
HACKER_REVIEWER = "Hacker Reviewer"
MENTOR_REVIEWER = "Mentor Reviewer"
VOLUNTEER_REVIEWER = "Volunteer Reviewer"
ORGANIZER = "Organizer"
VOLUNTEER = "Volunteer"
CHECKIN_LEAD = "Check-in Lead"
LEAD = "Lead" # Applications/Mentors/Volunteer Committee Leads
SPONSOR = "Sponsor"
JUDGE = "Judge"
WORKSHOP_LEAD = "Workshop Lead"
Expand Down
Loading

0 comments on commit 798d39e

Please sign in to comment.