Skip to content

Commit 6c5e2f1

Browse files
committed
Add button to download ranking
1 parent 7b88c34 commit 6c5e2f1

4 files changed

Lines changed: 54 additions & 0 deletions

File tree

backend/src/handlers/admin.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use axum::Json;
22
use axum::extract::{Path, State};
3+
use axum_extra::response::Attachment;
34
use tracing::info;
45

56
use super::ApiError;
@@ -34,6 +35,41 @@ pub async fn user_list(
3435
Ok(Json(resp))
3536
}
3637

38+
pub async fn download_ranking(
39+
State(state): State<AppState>,
40+
_: Admin,
41+
) -> Result<Attachment<Vec<u8>>, ApiError> {
42+
let tasks = database::get_tasks(&state.pool).await?;
43+
let task_names: Vec<&str> = tasks.iter().map(|task| task.name.as_str()).collect();
44+
45+
let users = database::get_users(&state.pool).await?;
46+
let mut resp = vec![];
47+
for u in users {
48+
resp.push(super::get_user_status(&state.pool, u).await?);
49+
}
50+
51+
let mut csv = csv::Writer::from_writer(vec![]);
52+
53+
let mut headers = vec!["name", "surname", "token", "role"];
54+
headers.append(&mut task_names.clone());
55+
headers.append(&mut vec!["total_score"]);
56+
57+
csv.write_record(&headers)?;
58+
59+
for user in resp {
60+
let mut row = vec![user.name, user.surname, user.token, user.role.to_string()];
61+
for t in &task_names {
62+
row.push(user.tasks.get(*t).map_or(0f64, |x| x.score).to_string());
63+
}
64+
row.push(user.total_score.to_string());
65+
csv.write_record(row)?;
66+
}
67+
68+
Ok(Attachment::new(csv.into_inner().unwrap())
69+
.content_type("text/csv")
70+
.filename("ranking.csv"))
71+
}
72+
3773
pub async fn questions(
3874
State(state): State<AppState>,
3975
_: Admin,

backend/src/handlers/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ impl From<serde_json::Error> for ApiError {
5858
}
5959
}
6060

61+
impl From<csv::Error> for ApiError {
62+
fn from(err: csv::Error) -> Self {
63+
ApiError::Internal(err.to_string())
64+
}
65+
}
66+
6167
impl From<sqlx::Error> for ApiError {
6268
fn from(err: sqlx::Error) -> Self {
6369
ApiError::Internal(err.to_string())

backend/src/serve.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ fn create_router(app_state: AppState) -> Router {
8888
post(admin::set_extra_time),
8989
)
9090
.route("/api/admin/user_list", get(admin::user_list))
91+
.route("/api/admin/ranking", get(admin::download_ranking))
9192
.route("/api/admin/questions", get(admin::questions))
9293
.route(
9394
"/api/admin/answer_question/{id}",

frontend/src/admin/AdminContestStatus.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useMemo, useState } from "react";
22
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
33
import {
4+
faDownload,
45
faHourglassStart,
56
faSort,
67
faSortDown,
@@ -230,6 +231,16 @@ export function ContestStatusView() {
230231
<option value="30000">30s</option>
231232
<option value="120000">2m</option>
232233
</select>
234+
<a
235+
role="button"
236+
className="btn btn-light ml-3"
237+
href={`${client.apiBaseURI}admin/ranking`}
238+
download
239+
>
240+
<FontAwesomeIcon icon={faDownload} />
241+
{" "}
242+
CSV
243+
</a>
233244
</div>
234245
<table className="table table-striped">
235246
<thead>

0 commit comments

Comments
 (0)