Skip to content
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

[Admin] Set hacker score thresholds #527

Merged
merged 4 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions apps/api/src/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,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;
Loading