Skip to content
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
17 changes: 17 additions & 0 deletions src/kernelbot/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,23 @@ async def admin_delete_submission(
return {"status": "ok", "submission_id": submission_id}


@app.delete("/admin/submissions")
async def admin_delete_submissions_for_user(
leaderboard_id: int,
user_name: str,
_: Annotated[None, Depends(require_admin)],
db_context=Depends(get_db),
) -> dict:
with db_context as db:
deleted = db.delete_submissions_for_user(leaderboard_id, user_name)
return {
"status": "ok",
"leaderboard_id": leaderboard_id,
"user_name": user_name,
**deleted,
}


@app.get("/admin/stats")
async def admin_stats(
_: Annotated[None, Depends(require_admin)],
Expand Down
66 changes: 66 additions & 0 deletions src/libkernelbot/leaderboard_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,72 @@ def delete_submission(self, submission_id: int):
logger.exception("Could not delete submission %s.", submission_id, exc_info=e)
raise KernelBotError(f"Could not delete submission {submission_id}!") from e

def delete_submissions_for_user(self, leaderboard_id: int, user_name: str) -> dict[str, int]:
try:
self.cursor.execute(
"""
SELECT 1
FROM leaderboard.leaderboard
WHERE id = %s
""",
(leaderboard_id,),
)
if self.cursor.fetchone() is None:
raise LeaderboardDoesNotExist(str(leaderboard_id))

self.cursor.execute(
"""
WITH target_submissions AS (
SELECT s.id
FROM leaderboard.submission s
JOIN leaderboard.user_info ui ON ui.id = s.user_id
WHERE s.leaderboard_id = %s
AND ui.user_name = %s
),
deleted_job_status AS (
DELETE FROM leaderboard.submission_job_status sjs
WHERE sjs.submission_id IN (SELECT id FROM target_submissions)
RETURNING sjs.id
),
deleted_runs AS (
DELETE FROM leaderboard.runs r
WHERE r.submission_id IN (SELECT id FROM target_submissions)
RETURNING r.id
),
deleted_submissions AS (
DELETE FROM leaderboard.submission s
WHERE s.id IN (SELECT id FROM target_submissions)
RETURNING s.id
)
SELECT
(SELECT COUNT(*) FROM deleted_job_status) AS deleted_job_status,
(SELECT COUNT(*) FROM deleted_runs) AS deleted_runs,
(SELECT COUNT(*) FROM deleted_submissions) AS deleted_submissions
""",
(leaderboard_id, user_name),
)
deleted_job_status, deleted_runs, deleted_submissions = self.cursor.fetchone()
self.connection.commit()
return {
"deleted_job_status": deleted_job_status,
"deleted_runs": deleted_runs,
"deleted_submissions": deleted_submissions,
}
except KernelBotError:
self.connection.rollback()
raise
except psycopg2.Error as e:
self.connection.rollback()
logger.exception(
"Could not delete submissions for leaderboard %s user %s.",
leaderboard_id,
user_name,
exc_info=e,
)
raise KernelBotError(
f"Could not delete submissions for leaderboard {leaderboard_id} and user {user_name}!"
) from e

def get_user_submissions(
self,
user_id: str,
Expand Down
25 changes: 25 additions & 0 deletions tests/test_admin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,31 @@ def test_delete_submission(self, test_client, mock_backend):
assert response.status_code == 200
mock_backend.db.delete_submission.assert_called_once_with(123)

def test_delete_submissions_for_user(self, test_client, mock_backend):
"""DELETE /admin/submissions deletes by leaderboard ID and username."""
mock_backend.db.__enter__ = MagicMock(return_value=mock_backend.db)
mock_backend.db.__exit__ = MagicMock(return_value=None)
mock_backend.db.delete_submissions_for_user = MagicMock(return_value={
"deleted_job_status": 2,
"deleted_runs": 5,
"deleted_submissions": 3,
})

response = test_client.delete(
"/admin/submissions?leaderboard_id=765&user_name=Borui%20Xu",
headers={"Authorization": "Bearer test_token"}
)
assert response.status_code == 200
assert response.json() == {
"status": "ok",
"leaderboard_id": 765,
"user_name": "Borui Xu",
"deleted_job_status": 2,
"deleted_runs": 5,
"deleted_submissions": 3,
}
mock_backend.db.delete_submissions_for_user.assert_called_once_with(765, "Borui Xu")


class TestAdminLeaderboards:
"""Test admin leaderboard endpoints."""
Expand Down
64 changes: 63 additions & 1 deletion tests/test_leaderboard_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,69 @@ def test_delete_leaderboard(database, submit_leaderboard):
assert db.get_leaderboard_names() == []


def test_delete_submissions_for_user(database, submit_leaderboard):
with database as db:
target_a = db.create_submission(
"submit-leaderboard",
"submission.py",
5,
"pass",
datetime.datetime.now(),
user_name="target-user",
)
target_b = db.create_submission(
"submit-leaderboard",
"submission.py",
5,
"pass 2",
datetime.datetime.now(),
user_name="target-user",
)
other = db.create_submission(
"submit-leaderboard",
"submission.py",
7,
"different",
datetime.datetime.now(),
user_name="other-user",
)

_create_submission_run(db, target_a, mode="leaderboard", secret=False, runner="A100", score=5)
_create_submission_run(db, target_b, mode="test", secret=False, runner="A100", score=None)
_create_submission_run(db, other, mode="leaderboard", secret=False, runner="A100", score=7)
db.upsert_submission_job_status(target_a, "running", None)
db.upsert_submission_job_status(target_b, "pending", None)

deleted = db.delete_submissions_for_user(db.get_leaderboard_id("submit-leaderboard"), "target-user")

assert deleted == {
"deleted_job_status": 2,
"deleted_runs": 2,
"deleted_submissions": 2,
}
assert db.get_submission_by_id(target_a) is None
assert db.get_submission_by_id(target_b) is None
assert db.get_submission_by_id(other) is not None

db.cursor.execute("SELECT COUNT(*) FROM leaderboard.runs")
assert db.cursor.fetchone()[0] == 1

db.cursor.execute("SELECT COUNT(*) FROM leaderboard.submission_job_status")
assert db.cursor.fetchone()[0] == 0

db.cursor.execute("SELECT COUNT(*) FROM leaderboard.submission")
assert db.cursor.fetchone()[0] == 1


def test_delete_submissions_for_user_missing_leaderboard(database):
with database as db:
with pytest.raises(
leaderboard_db.LeaderboardDoesNotExist,
match="Leaderboard `999999` does not exist.",
):
db.delete_submissions_for_user(999999, "target-user")


def test_delete_leaderboard_with_runs(database, submit_leaderboard):
with database as db:
db.create_submission(
Expand Down Expand Up @@ -1135,4 +1198,3 @@ def test_check_rate_limit_categories_independent(database, submit_leaderboard):
# Test should be blocked
result = db.check_rate_limit("submit-leaderboard", "123", "test")
assert result["allowed"] is False

Loading