Skip to content

Commit

Permalink
[Admin] Set hacker score thresholds (#527)
Browse files Browse the repository at this point in the history
* - Added /set-thresholds route
- Added UI inputs in HackerApplicants.tsx
- Added /get-thresholds route
- Display current thresholds in HackerApplicants.tsx

* updates thresholds only if non-empty str is passed

* Reverted to using fastapi validation for floats
  • Loading branch information
IanWearsHat authored Jan 4, 2025
1 parent 7d9e46b commit 236a36c
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 1 deletion.
57 changes: 57 additions & 0 deletions apps/api/src/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,63 @@ async def submit_review(
)


@router.post("/set-thresholds")
async def set_hacker_score_thresholds(
user: Annotated[User, Depends(require_manager)],
accept: float = Body(),
waitlist: float = Body(),
) -> None:
"""
Sets accepted and waitlisted score thresholds.
Any score under waitlisted is considered rejected.
"""
log.info("%s changed thresholds: Accept-%f | Waitlist-%f", user, accept, waitlist)

# negative numbers should not be received, but -1 in this case
# means there is no update to the respective threshold
update_query = {}
if accept != -1:
update_query["accept"] = accept
if waitlist != -1:
update_query["waitlist"] = waitlist

try:
await mongodb_handler.raw_update_one(
Collection.SETTINGS,
{"_id": "hacker_score_thresholds"},
{"$set": update_query},
upsert=True,
)
except RuntimeError:
log.error(
"%s could not change thresholds: Accept-%f | Waitlist-%f",
user,
accept,
waitlist,
)
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)


@router.get("/get-thresholds")
async def get_hacker_score_thresholds(
user: Annotated[User, Depends(require_manager)]
) -> Optional[dict[str, Any]]:
"""
Gets accepted and waitlisted thresholds
"""
log.info("%s requested thresholds", user)

try:
record = await mongodb_handler.retrieve_one(
Collection.SETTINGS,
{"_id": "hacker_score_thresholds"},
)
except RuntimeError:
log.error("%s could not retrieve thresholds", user)
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
return record


@router.post("/release", dependencies=[Depends(require_director)])
async def release_decisions() -> None:
"""Update applicant status based on decision and send decision emails."""
Expand Down
44 changes: 44 additions & 0 deletions apps/api/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,47 @@ def test_release_hacker_decisions_works(

assert res.status_code == 200
assert returned_records[0]["decision"] == Decision.ACCEPTED


@patch("services.mongodb_handler.retrieve_one", autospec=True)
@patch("services.mongodb_handler.raw_update_one", autospec=True)
def test_set_thresholds_correctly(
mock_mongodb_handler_raw_update_one: AsyncMock,
mock_mongodb_handler_retrieve_one: AsyncMock,
) -> None:
"""Test that the /set-thresholds route returns correctly"""
mock_mongodb_handler_retrieve_one.return_value = REVIEWER_IDENTITY

res = reviewer_client.post(
"/set-thresholds", json={"accept": "12", "waitlist": "5"}
)

assert res.status_code == 200
mock_mongodb_handler_raw_update_one.assert_awaited_once_with(
Collection.SETTINGS,
{"_id": "hacker_score_thresholds"},
{"$set": {"accept": 12, "waitlist": 5}},
upsert=True,
)


@patch("services.mongodb_handler.retrieve_one", autospec=True)
@patch("services.mongodb_handler.raw_update_one", autospec=True)
def test_set_thresholds_with_empty_string_correctly(
mock_mongodb_handler_raw_update_one: AsyncMock,
mock_mongodb_handler_retrieve_one: AsyncMock,
) -> None:
"""Test that the /set-thresholds route returns correctly with -1"""
mock_mongodb_handler_retrieve_one.return_value = REVIEWER_IDENTITY

res = reviewer_client.post(
"/set-thresholds", json={"accept": "12", "waitlist": "-1"}
)

assert res.status_code == 200
mock_mongodb_handler_raw_update_one.assert_awaited_once_with(
Collection.SETTINGS,
{"_id": "hacker_score_thresholds"},
{"$set": {"accept": 12}},
upsert=True,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useState } from "react";
import axios from "axios";

import {
Box,
Button,
Input,
SpaceBetween,
} from "@cloudscape-design/components";

import useHackerThresholds from "@/lib/admin/useHackerThresholds";

function HackerThresholdInputs() {
const { thresholds } = useHackerThresholds();

const [acceptValue, setAcceptValue] = useState("");
const [waitlistValue, setWaitlistValue] = useState("");

const [status, setStatus] = useState("");

async function submitThresholds() {
const sentAcceptValue = acceptValue ? acceptValue : -1;
const sentWaitlistValue = waitlistValue ? waitlistValue : -1;

await axios
.post("/api/admin/set-thresholds", {
accept: sentAcceptValue,
waitlist: sentWaitlistValue,
})
.then((response) => {
// TODO: Add flashbar or modal to show post status
if (response.status === 200) {
setStatus(
"Successfully updated thresholds. Reload to see changes to applicants.",
);
} else {
setStatus("Failed to update thresholds");
}
});
}

return (
<SpaceBetween direction="vertical" size="xs">
{thresholds && (
<>
<Box variant="awsui-key-label">
Current Accept Threshold: {thresholds.accept}
</Box>
<Box variant="awsui-key-label">
Current Waitlist Threshold: {thresholds.waitlist}
</Box>
</>
)}
<Box variant="awsui-key-label">Accept Threshold</Box>
<Input
onChange={({ detail }) => setAcceptValue(detail.value)}
value={acceptValue}
type="number"
inputMode="decimal"
placeholder="Accept Threshold"
step={0.1}
/>
<Box variant="awsui-key-label">Waitlist Threshold</Box>
<Input
onChange={({ detail }) => setWaitlistValue(detail.value)}
value={waitlistValue}
type="number"
inputMode="decimal"
placeholder="Waitlist Threshold"
step={0.1}
/>
<Box variant="p">
Any score under{" "}
{waitlistValue ? waitlistValue : "the waitlist threshold"} will be
rejected
</Box>
<Button variant="primary" onClick={submitThresholds}>
Update Thresholds
</Button>
{status ? (
<Box variant="awsui-key-label" color="text-status-warning">
{status}
</Box>
) : null}
</SpaceBetween>
);
}

export default HackerThresholdInputs;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ApplicantStatus from "../components/ApplicantStatus";

import UserContext from "@/lib/admin/UserContext";
import { isApplicationManager } from "@/lib/admin/authorization";
import HackerThresholdInputs from "../components/HackerThresholdInputs";
import ApplicantReviewerIndicator from "../components/ApplicantReviewerIndicator";

function HackerApplicants() {
Expand Down Expand Up @@ -116,7 +117,11 @@ function HackerApplicants() {
/>
}
empty={emptyContent}
header={<Header counter={counter}>Applicants</Header>}
header={
<Header counter={counter} actions={<HackerThresholdInputs />}>
Applicants
</Header>
}
/>
);
}
Expand Down
24 changes: 24 additions & 0 deletions apps/site/src/lib/admin/useHackerThresholds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import axios from "axios";
import useSWR from "swr";

export interface ScoreThresholds {
_id: string;
accept: number;
waitlist: number;
}

const fetcher = async (url: string) => {
const res = await axios.get<ScoreThresholds>(url);
return res.data;
};

function useHackerThresholds() {
const { data, error, isLoading } = useSWR<ScoreThresholds>(
"/api/admin/get-thresholds",
fetcher,
);

return { thresholds: data, loading: isLoading, error };
}

export default useHackerThresholds;

0 comments on commit 236a36c

Please sign in to comment.