diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index f085e67..e0491f0 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -35,6 +35,7 @@ export interface LeaderboardDetail { score: number; user_name: string; submission_id: number; + line_count?: number; }> >; } @@ -43,7 +44,6 @@ export interface CodesResponse { results: Array<{ submission_id: number; code: string; - line_count?: number; }>; } diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index 1b9e1b6..205a97e 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -417,7 +417,6 @@ function LeaderboardWithSidebar() { navigationItems, navigationIndex, codes, - lineCounts, isOpen, isLoadingCodes, navigate, @@ -456,7 +455,6 @@ function LeaderboardWithSidebar() { navigationItems={navigationItems} navigationIndex={navigationIndex} codes={codes} - lineCounts={lineCounts} isLoadingCodes={isLoadingCodes} onClose={close} onNavigate={navigate} diff --git a/frontend/src/pages/leaderboard/components/RankingLists.tsx b/frontend/src/pages/leaderboard/components/RankingLists.tsx index ebe4b5c..26cbf2f 100644 --- a/frontend/src/pages/leaderboard/components/RankingLists.tsx +++ b/frontend/src/pages/leaderboard/components/RankingLists.tsx @@ -29,6 +29,7 @@ interface RankingItem { submission_id: number; submission_count?: number; submission_time?: string; + line_count?: number; } interface RankingsListProps { @@ -84,6 +85,10 @@ const styles: Record> = { fontFamily: "monospace", color: "text.secondary", }, + lineCount: { + fontFamily: "monospace", + color: "text.secondary", + }, }; export default function RankingsList({ @@ -160,6 +165,13 @@ export default function RankingsList({ } const isExpanded = expanded[field] ?? true; const visibleItems = isExpanded ? items : items.slice(0, 3); + const showLineCount = expired && items.some( + (item) => typeof item.line_count === "number", + ); + const scoreSize = isAdmin ? 2 : showLineCount ? 2 : 3; + const deltaSize = isAdmin ? 1 : showLineCount ? 2 : 3; + const fileSize = 3; + const adminSubmissionCountSize = showLineCount ? 1 : 2; return ( @@ -189,18 +201,18 @@ export default function RankingsList({ {item.user_name} {getMedalIcon(item.rank)} - + {formatMicroseconds(item.score)} - + {item.prev_score > 0 && `+${formatMicroseconds(item.prev_score)}`} - + + {showLineCount && ( + + + {typeof item.line_count === "number" + ? `LOC: ${item.line_count.toLocaleString()}` + : ""} + + + )} {isAdmin && ( - + Subs: {item.submission_count} diff --git a/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.test.tsx b/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.test.tsx deleted file mode 100644 index 43eb575..0000000 --- a/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; -import { renderWithProviders } from "../../../tests/test-utils"; -import SubmissionCodeSidebar from "./SubmissionCodeSidebar"; - -describe("SubmissionCodeSidebar", () => { - it("shows LOC when line count metadata is available", () => { - renderWithProviders( - , - ); - - expect(screen.getByText("LOC")).toBeInTheDocument(); - expect(screen.getByText("2")).toBeInTheDocument(); - }); - - it("does not show LOC without line count metadata", () => { - renderWithProviders( - , - ); - - expect(screen.queryByText("LOC")).not.toBeInTheDocument(); - }); -}); diff --git a/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx b/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx index 29132b2..77a44a1 100644 --- a/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx +++ b/frontend/src/pages/leaderboard/components/SubmissionCodeSidebar.tsx @@ -22,7 +22,6 @@ interface SubmissionCodeSidebarProps { navigationItems: NavigationItem[]; navigationIndex: number; codes: Map; - lineCounts?: Map; isLoadingCodes?: boolean; onClose: () => void; onNavigate: (newIndex: number, item: NavigationItem) => void; @@ -35,7 +34,6 @@ export default function SubmissionCodeSidebar({ navigationItems, navigationIndex, codes, - lineCounts, isLoadingCodes = false, onClose, onNavigate, @@ -93,9 +91,6 @@ export default function SubmissionCodeSidebar({ }, [onWidthChange]); const drawerWidth = isMobile ? "100vw" : width; - const lineCount = selectedSubmission - ? lineCounts?.get(selectedSubmission.submissionId) - : undefined; return ( )} - {lineCount !== undefined && ( - - - LOC - - - {lineCount.toLocaleString()} - - - )} {selectedSubmission.fileName && ( ; - lineCounts: Map; isOpen: boolean; isLoadingCodes: boolean; navigate: (newIndex: number, item: NavigationItem) => void; @@ -41,7 +40,6 @@ export function SubmissionSidebarProvider({ const [navigationItems, setNavigationItems] = useState([]); const [navigationIndex, setNavigationIndex] = useState(0); const [codes, setCodes] = useState>(new Map()); - const [lineCounts, setLineCounts] = useState>(new Map()); const [isLoadingCodes, setIsLoadingCodes] = useState(false); const codesRef = useRef(codes); codesRef.current = codes; @@ -74,15 +72,6 @@ export function SubmissionSidebarProvider({ } return next; }); - setLineCounts((prev) => { - const next = new Map(prev); - for (const item of response?.results ?? []) { - if (typeof item.line_count === "number") { - next.set(item.submission_id, item.line_count); - } - } - return next; - }); }) .catch((err) => { console.warn("[SubmissionSidebar] Failed to fetch codes:", err); @@ -124,7 +113,6 @@ export function SubmissionSidebarProvider({ navigationItems, navigationIndex, codes, - lineCounts, isOpen: !!selectedSubmission, isLoadingCodes, navigate, diff --git a/kernelboard/api/leaderboard.py b/kernelboard/api/leaderboard.py index c5b9ea0..c7e5c97 100644 --- a/kernelboard/api/leaderboard.py +++ b/kernelboard/api/leaderboard.py @@ -141,7 +141,8 @@ def _get_query(): ORDER BY CASE WHEN lang = 'Python' THEN 0 ELSE 1 END LIMIT 1 ) AS starter, - task->'benchmarks' AS benchmarks + task->'benchmarks' AS benchmarks, + deadline < NOW() AS show_solution_code_metadata FROM leaderboard.leaderboard WHERE id = %(leaderboard_id)s ), @@ -179,11 +180,25 @@ def _get_query(): s.file_name AS file_name, r.submission_id AS submission_id, COALESCE(sc.submission_count, 0) AS submission_count, + CASE + WHEN cf.code IS NULL THEN NULL + WHEN cf.code = '' THEN 0 + ELSE array_length( + regexp_split_to_array( + regexp_replace(cf.code, E'(\\r\\n|\\r|\\n)$', ''), + E'\\r\\n|\\r|\\n' + ), + 1 + ) + END AS line_count, RANK() OVER (PARTITION BY r.runner, u.id ORDER BY r.score ASC) AS rank FROM leaderboard.runs r JOIN leaderboard.submission s ON r.submission_id = s.id LEFT JOIN leaderboard.user_info u ON s.user_id = u.id LEFT JOIN submission_counts sc ON s.user_id = sc.user_id AND r.runner = sc.runner + LEFT JOIN leaderboard.code_files cf + ON cf.id = s.code_id + AND (SELECT show_solution_code_metadata FROM leaderboard_info) WHERE NOT r.secret AND r.score IS NOT NULL AND r.passed @@ -220,6 +235,7 @@ def _get_query(): SELECT jsonb_build_object( 'rankings', (SELECT jsonb_object_agg(g.gpu_type, ( SELECT jsonb_agg( + ( jsonb_build_object( 'user_name', r.user_name, 'score', r.score, @@ -228,6 +244,12 @@ def _get_query(): 'submission_count', r.submission_count, 'submission_time', r.submission_time ) + || + CASE + WHEN r.line_count IS NULL THEN '{}'::jsonb + ELSE jsonb_build_object('line_count', r.line_count) + END + ) ORDER BY r.score ASC ) FROM top_runs r WHERE r.runner = g.gpu_type))), diff --git a/kernelboard/api/submission.py b/kernelboard/api/submission.py index 6830e9f..abf6e08 100644 --- a/kernelboard/api/submission.py +++ b/kernelboard/api/submission.py @@ -35,10 +35,6 @@ WEB_AUTH_HEADER = "X-Web-Auth-Id" MAX_CONTENT_LENGTH = 1 * 1024 * 1024 # 1MB max file size -# This blocks the leaderboard to show all the ranking codes when the leaderboard is ended -BLOCKED_CODE_LEADERBOARD_LIST: list[str] = ["598"] # leaderboard id to block show - - USE_MOCK_SUBMISSION: bool = os.environ.get( "USE_MOCK_SUBMISSION", "" ).lower() == "true" @@ -184,22 +180,15 @@ def list_codes_route(): ) try: - # if leaderboard is allowed, allow all users to see the leaderboard codes is_ended = is_leaderboard_ended(leaderboard_id) - is_allowed = str(leaderboard_id) not in BLOCKED_CODE_LEADERBOARD_LIST logger.info("[list_codes] leaderboard is ended: %s", is_ended) - logger.info("[list_codes] leaderboard is allowed: %s", is_allowed) - # if leaderboard is ended and allowed, allow all users to see the leaderboard codes - if is_ended and is_allowed: + # if leaderboard is ended, allow all users to see the leaderboard codes + if is_ended: logger.info( - "[list_codes] leaderboard is allowed, allow all users to see the leaderboard codes" - ) - results = list_codes( - leaderboard_id, - submission_ids, - include_line_count=True, + "[list_codes] leaderboard is ended, allow all users to see the leaderboard codes" ) + results = list_codes(leaderboard_id, submission_ids) return http_success( data={"results": results}, ) @@ -299,35 +288,24 @@ def list_submissions(): def list_codes( leaderboard_id: int, submission_ids: List[int], - *, - include_line_count: bool = False, -) -> List[dict[str, Any]]: +) -> Tuple[List[dict[str, Any]], str]: conn = get_db_connection() with conn.cursor() as cur: sql, params = _query_list_codes(leaderboard_id, submission_ids) cur.execute(sql, params) rows = cur.fetchall() - items = [] - for r in rows: - code = decodeCodeText(r[3]) - item = { + items = [ + { "submission_id": r[0], "leaderboard_id": r[1], "code_id": r[2], - "code": code, + "code": decodeCodeText(r[3]), } - if include_line_count: - item["line_count"] = count_lines_of_code(code) - items.append(item) + for r in rows + ] return items -def count_lines_of_code(code_text: str) -> int: - if not code_text: - return 0 - return len(code_text.splitlines()) - - def decodeCodeText(code_text): if isinstance(code_text, memoryview): return code_text.tobytes().decode("utf-8") diff --git a/tests/api/test_submission_api.py b/tests/api/test_submission_api.py index 4fb3860..3a4428d 100644 --- a/tests/api/test_submission_api.py +++ b/tests/api/test_submission_api.py @@ -193,51 +193,6 @@ def _post_submission(client, form_overrides=None, file_tuple=None): ) -def _seed_code_submission( - app, - *, - leaderboard_id: int, - submission_id: int, - code_id: int, - code: str, -) -> None: - with app.app_context(): - conn = get_db_connection() - with conn: - with conn.cursor() as cur: - cur.execute( - """ - INSERT INTO leaderboard.user_info (id, user_name) - VALUES (%s, %s) - ON CONFLICT (id) DO UPDATE - SET user_name = EXCLUDED.user_name - """, - ("loc-test-user", "loc-test-user"), - ) - cur.execute( - """ - INSERT INTO leaderboard.code_files (id, code) - VALUES (%s, %s) - """, - (code_id, code), - ) - cur.execute( - """ - INSERT INTO leaderboard.submission - (id, leaderboard_id, file_name, user_id, code_id, submission_time, done) - VALUES - (%s, %s, %s, %s, %s, NOW(), TRUE) - """, - ( - submission_id, - leaderboard_id, - "loc_solution.py", - "loc-test-user", - code_id, - ), - ) - - def test_submission_happy_path(app, client, prepare): # auth + web_token default to True prepare() @@ -358,88 +313,6 @@ def test_submission_upstream_non_200_maps_to_http_error(app, client, prepare): assert "invalid format" in js["message"].lower() -# ---------------------------- -# /api/codes list tests -# ---------------------------- - -def test_list_codes_includes_line_count_for_ended_leaderboard(app, client, prepare): - prepare() - code = "import torch\n\nprint('ok')\n" - _seed_code_submission( - app, - leaderboard_id=339, - submission_id=910001, - code_id=910001, - code=code, - ) - - with app.app_context(): - conn = get_db_connection() - with conn: - with conn.cursor() as cur: - cur.execute( - """ - UPDATE leaderboard.leaderboard - SET deadline = NOW() - INTERVAL '1 hour' - WHERE id = 339 - """ - ) - - resp = client.post( - "/api/codes", - json={"leaderboard_id": 339, "submission_ids": [910001]}, - ) - - assert resp.status_code == http.HTTPStatus.OK - item = resp.get_json()["data"]["results"][0] - assert item["code"] == code - assert item["line_count"] == 3 - - -def test_list_codes_omits_line_count_for_active_admin_access( - app, - client, - monkeypatch, -): - fake_admin = SimpleNamespace( - is_anonymous=False, - is_authenticated=True, - get_id=lambda: "discord:838132355075014667", - ) - monkeypatch.setattr(flask_login.utils, "_get_user", lambda: fake_admin) - - code = "import torch\nprint('active')\n" - _seed_code_submission( - app, - leaderboard_id=339, - submission_id=910002, - code_id=910002, - code=code, - ) - - with app.app_context(): - conn = get_db_connection() - with conn: - with conn.cursor() as cur: - cur.execute( - """ - UPDATE leaderboard.leaderboard - SET deadline = NOW() + INTERVAL '1 hour' - WHERE id = 339 - """ - ) - - resp = client.post( - "/api/codes", - json={"leaderboard_id": 339, "submission_ids": [910002]}, - ) - - assert resp.status_code == http.HTTPStatus.OK - item = resp.get_json()["data"]["results"][0] - assert item["code"] == code - assert "line_count" not in item - - # ---------------------------- # /api/submissions list tests # ----------------------------