diff --git a/src/kernelbot/api/main.py b/src/kernelbot/api/main.py index 8abd859b..7c57f47e 100644 --- a/src/kernelbot/api/main.py +++ b/src/kernelbot/api/main.py @@ -689,6 +689,25 @@ async def admin_delete_leaderboard( return {"status": "ok", "leaderboard": leaderboard_name, "force": force} +@app.get("/admin/leaderboards/{leaderboard_name}/submissions") +async def admin_list_leaderboard_submissions( + leaderboard_name: str, + _: Annotated[None, Depends(require_admin)], + db_context=Depends(get_db), + limit: int = 100, + offset: int = 0, +) -> dict: + with db_context as db: + submissions = db.get_leaderboard_submission_history(leaderboard_name, limit, offset) + return { + "status": "ok", + "leaderboard": leaderboard_name, + "limit": limit, + "offset": offset, + "submissions": submissions, + } + + @app.delete("/admin/submissions/{submission_id}") async def admin_delete_submission( submission_id: int, diff --git a/src/libkernelbot/leaderboard_db.py b/src/libkernelbot/leaderboard_db.py index a3f2b795..91d8bc42 100644 --- a/src/libkernelbot/leaderboard_db.py +++ b/src/libkernelbot/leaderboard_db.py @@ -1276,6 +1276,55 @@ def get_user_submissions( logger.exception("Error fetching user submissions for user %s", user_id, exc_info=e) raise KernelBotError("Error fetching user submissions") from e + def get_leaderboard_submission_history( + self, + leaderboard_name: str, + limit: int = 100, + offset: int = 0, + ) -> list[dict]: + """Get all submissions for a leaderboard with admin-oriented metadata.""" + limit = max(1, min(limit, 1000)) + offset = max(0, offset) + + try: + self.get_leaderboard_id(leaderboard_name) + self.cursor.execute( + """ + SELECT s.id, lb.name, s.file_name, s.user_id, ui.user_name, + s.submission_time, s.done + FROM leaderboard.submission s + JOIN leaderboard.leaderboard lb ON s.leaderboard_id = lb.id + LEFT JOIN leaderboard.user_info ui ON ui.id = s.user_id + WHERE lb.name = %s + ORDER BY s.submission_time DESC, s.id DESC + LIMIT %s OFFSET %s + """, + (leaderboard_name, limit, offset), + ) + return [ + { + "id": row[0], + "leaderboard_name": row[1], + "file_name": row[2], + "user_id": row[3], + "user_name": row[4], + "submission_time": row[5], + "done": row[6], + } + for row in self.cursor.fetchall() + ] + except KernelBotError: + self.connection.rollback() + raise + except psycopg2.Error as e: + self.connection.rollback() + logger.exception( + "Error fetching submissions for leaderboard %s", + leaderboard_name, + exc_info=e, + ) + raise KernelBotError("Error fetching leaderboard submissions") from e + def get_submission_by_id(self, submission_id: int) -> Optional["SubmissionItem"]: query = """ SELECT s.leaderboard_id, lb.name, s.file_name, s.user_id, diff --git a/tests/test_admin_api.py b/tests/test_admin_api.py index 46d7fd48..48f6a166 100644 --- a/tests/test_admin_api.py +++ b/tests/test_admin_api.py @@ -141,6 +141,66 @@ def test_admin_stats_with_leaderboard_name(self, test_client, mock_backend): class TestAdminSubmissions: """Test admin submission endpoints.""" + def test_list_leaderboard_submissions(self, test_client, mock_backend): + """GET /admin/leaderboards/{name}/submissions returns all submission metadata.""" + mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db) + mock_backend.db.__exit__ = MagicMock(return_value=None) + mock_backend.db.get_leaderboard_submission_history = MagicMock(return_value=[ + { + "id": 123, + "leaderboard_name": "test-lb", + "file_name": "submission.py", + "user_id": "42", + "user_name": "alice", + "submission_time": "2026-04-07T12:00:00Z", + "done": True, + }, + { + "id": 122, + "leaderboard_name": "test-lb", + "file_name": "submission_old.py", + "user_id": "43", + "user_name": "bob", + "submission_time": "2026-04-07T11:00:00Z", + "done": False, + }, + ]) + + response = test_client.get( + "/admin/leaderboards/test-lb/submissions?limit=50&offset=10", + headers={"Authorization": "Bearer test_token"}, + ) + assert response.status_code == 200 + assert response.json() == { + "status": "ok", + "leaderboard": "test-lb", + "limit": 50, + "offset": 10, + "submissions": [ + { + "id": 123, + "leaderboard_name": "test-lb", + "file_name": "submission.py", + "user_id": "42", + "user_name": "alice", + "submission_time": "2026-04-07T12:00:00Z", + "done": True, + }, + { + "id": 122, + "leaderboard_name": "test-lb", + "file_name": "submission_old.py", + "user_id": "43", + "user_name": "bob", + "submission_time": "2026-04-07T11:00:00Z", + "done": False, + }, + ], + } + mock_backend.db.get_leaderboard_submission_history.assert_called_once_with( + "test-lb", 50, 10 + ) + def test_get_submission(self, test_client, mock_backend): """GET /admin/submissions/{id} returns submission.""" mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db)