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

Rework stats #42

Merged
merged 7 commits into from
Nov 27, 2023
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const_format = "0.2.32"
pin-project-lite = "0.2.13"
reqwest = { version = "0.11.22", default-features = false, features = ["json", "rustls-tls"] }
libiam = { git = "https://github.com/Verseghy/iam", package = "libiam" }
chrono = "0.4.31"
chrono = { version = "0.4.31", features = ["serde"] }

[workspace.dependencies]
uuid = { version = "1.5.0", features = ["v4", "fast-rng", "serde"] }
Expand Down
2 changes: 1 addition & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3'

services:
db:
image: postgres:15
image: postgres:16
environment:
POSTGRES_USER: matverseny
POSTGRES_PASSWORD: secret
Expand Down
2 changes: 1 addition & 1 deletion migration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ path = "src/lib.rs"
workspace = true

[dependencies]
tokio = { version = "1.33.0", features = ["rt", "macros"] }
tokio = { version = "1.33.0", features = ["rt-multi-thread", "macros"] }
sea-orm-migration = { version = "0.12.4", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls", "cli"] }
entity = { path = "../entity" }
const_format = "0.2.32"
6 changes: 4 additions & 2 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ pub fn routes<S: StateTrait>(state: S) -> Router<S> {
.route("/ws", get(socket::ws_handler::<S>))
.route(
"/stats",
get(stats::get_stats::<S>
.layer(PermissionsLayer::new(state, &["mathcompetition.admin"]))),
post(
stats::get_stats::<S>
.layer(PermissionsLayer::new(state, &["mathcompetition.admin"])),
),
)
.route("/liveness", get(|| async {}))
.route("/readiness", get(|| async {}))
Expand Down
241 changes: 176 additions & 65 deletions src/handlers/stats.rs
Original file line number Diff line number Diff line change
@@ -1,86 +1,197 @@
use std::collections::BTreeMap;

use crate::{error::Result, json::Json, StateTrait};
use axum::extract::State;
use entity::{problems, solutions_history, teams, users};
use sea_orm::{ColumnTrait, EntityTrait, FromQueryResult, QueryFilter, QueryOrder, QuerySelect};
use serde::Serialize;
use chrono::{DateTime, Utc};
use sea_orm::{ConnectionTrait, FromQueryResult, Statement};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Serialize, FromQueryResult)]
struct Member {
id: Uuid,
school: String,
class: i16,
#[derive(Debug, Deserialize)]
pub struct Request {
timestamp: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
struct Answer {
problem: Uuid,
answer: Option<i64>,
pub struct TeamData {
team_id: Uuid,
correct: i64,
wrong: i64,
}

#[derive(Debug, Serialize)]
struct Team {
id: Uuid,
name: String,
members: Vec<Member>,
answers: Vec<Answer>,
}
pub type Response = Vec<TeamData>;

#[derive(Debug, Serialize, FromQueryResult)]
struct Solution {
id: Uuid,
solution: i64,
}
pub async fn get_stats<S: StateTrait>(
State(state): State<S>,
Json(request): Json<Request>,
) -> Result<Json<Response>> {
static SQL: &str = "
select
id,
bools,
coalesce(count, 0) as count
from
teams
cross join (
values
(true),
(false)
) as bools(bools)
full outer join (
select
team,
correct,
count(*) as count
from
(
select
distinct on (team, problem) team,
solutions_history.solution = problems.solution as correct
from
solutions_history
inner join problems on problems.id = solutions_history.problem
where
created_at < $1
order by
team,
problem,
created_at desc
)
where
correct is not null
group by
team,
correct
) as correct on correct.team = teams.id
and correct.correct = bools
where
locked = true
order by
id,
bools;
";

#[derive(Debug, Serialize)]
pub struct Response {
teams: Vec<Team>,
solutions: Vec<Solution>,
}
let db = state.db();

pub async fn get_stats<S: StateTrait>(State(state): State<S>) -> Result<Json<Response>> {
let solutions = problems::Entity::find()
.select_only()
.column(problems::Column::Id)
.column(problems::Column::Solution)
.into_model::<Solution>()
.all(state.db())
let res = db
.query_all(Statement {
sql: SQL.to_owned(),
values: Some(sea_orm::Values(vec![request.timestamp.into()])),
db_backend: db.get_database_backend(),
})
.await?;

let teams = teams::Entity::find().all(state.db()).await?;
let mut map = BTreeMap::<Uuid, TeamData>::new();

let mut final_teams = Vec::with_capacity(teams.len());
for row in &res {
#[derive(FromQueryResult)]
struct Row {
id: Uuid,
bools: bool,
count: i64,
}

for team in teams {
let members = users::Entity::find_in_team(&team.id)
.into_model::<Member>()
.all(state.db())
.await?;
let row = Row::from_query_result(row, "")?;

let answers = solutions_history::Entity::find()
.filter(solutions_history::Column::Team.eq(team.id))
.distinct_on([solutions_history::Column::Problem])
.order_by_desc(solutions_history::Column::Problem)
.order_by_desc(solutions_history::Column::CreatedAt)
.all(state.db())
.await?;
let slot = match map.get_mut(&row.id) {
Some(s) => s,
None => {
map.insert(
row.id,
TeamData {
team_id: row.id,
correct: 0,
wrong: 0,
},
);
map.get_mut(&row.id).unwrap()
}
};

final_teams.push(Team {
id: team.id,
name: team.name,
members,
answers: answers
.into_iter()
.map(|x| Answer {
problem: x.problem,
answer: x.solution,
})
.collect(),
})
if row.bools {
slot.correct = row.count;
} else {
slot.wrong = row.count;
}
}

Ok(Json(Response {
teams: final_teams,
solutions,
}))
// let query = Query::select()
// .column(teams::Column::Id)
// .column(Alias::new("bools"))
// .expr_as(
// Func::coalesce([Expr::col(Alias::new("count")).into(), Expr::val(0).into()]),
// Alias::new("count"),
// )
// .from(teams::Entity)
// .join(
// JoinType::CrossJoin,
// TableRef::ValuesList(
// vec![
// ValueTuple::One(Value::Bool(Some(true))),
// ValueTuple::One(Value::Bool(Some(false))),
// ],
// Alias::new("bools(bools)").into_iden(),
// ),
// Cond::any(),
// )
// .join(
// JoinType::FullOuterJoin,
// TableRef::SubQuery(
// Query::select()
// .column(solutions_history::Column::Team)
// .column((Alias::new("sub1"), Alias::new("correct")))
// .expr_as(Func::count(Expr::col(Asterisk)), Alias::new("count"))
// .from_subquery(
// Query::select()
// .distinct_on([
// solutions_history::Column::Team,
// solutions_history::Column::Problem,
// ])
// .column(solutions_history::Column::Team)
// .expr_as(
// Expr::col(solutions_history::Column::Solution)
// .equals(problems::Column::Solution),
// Alias::new("correct"),
// )
// .from(solutions_history::Entity)
// .join(
// JoinType::InnerJoin,
// problems::Entity,
// Expr::col(problems::Column::Id)
// .eq(Expr::col(solutions_history::Column::Problem)),
// )
// .and_where(Expr::col(solutions_history::Column::Solution).is_not_null())
// .and_where(
// Expr::col(solutions_history::Column::CreatedAt)
// .lt(request.timestamp),
// )
// .order_by_columns([
// (solutions_history::Column::Team, Order::Desc),
// (solutions_history::Column::Problem, Order::Desc),
// (solutions_history::Column::CreatedAt, Order::Desc),
// ])
// .take(),
// Alias::new("sub1"),
// )
// .group_by_col(solutions_history::Column::Team)
// .group_by_col((Alias::new("sub1"), Alias::new("correct")))
// .to_owned(),
// Alias::new("correct").into_iden(),
// ),
// Cond::all()
// .add(
// Expr::col((Alias::new("correct"), Alias::new("team")))
// .eq(Expr::col(teams::Column::Id)),
// )
// .add(
// Expr::col((Alias::new("correct"), Alias::new("correct")))
// .eq(Expr::col(Alias::new("bools"))),
// ),
// )
// .and_where(teams::Column::Locked.eq(true))
// .to_owned();

let teams = map.into_values().collect();

Ok(Json(teams))
}
11 changes: 0 additions & 11 deletions tests/competition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,6 @@ use utils::prelude::*;
mod time {
use super::*;

// #[tokio::test]
// #[parallel]
// async fn get_not_admin() {
// let app = get_cached_app().await;
// let user = utils::iam::register_user().await;
//
// let res = app.post("/competition/time").user(&user).send().await;
//
// assert_error!(res, error::NOT_ENOUGH_PERMISSIONS);
// }

#[tokio::test]
#[parallel]
async fn put_not_admin() {
Expand Down
Loading