From 9d7836b5b05651e70f45350bb2698da1ffda325d Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 29 Oct 2024 22:44:03 +0100 Subject: [PATCH 1/5] added date range filter had no time yet to test if it is working --- challenges/src/endpoints/leaderboard.rs | 96 +++++++++-- challenges/src/services/leaderboard/global.rs | 33 +++- .../src/services/leaderboard/language.rs | 152 ++++++++++-------- challenges/src/services/leaderboard/task.rs | 25 ++- lib/src/services/skills.rs | 34 +++- 5 files changed, 246 insertions(+), 94 deletions(-) diff --git a/challenges/src/endpoints/leaderboard.rs b/challenges/src/endpoints/leaderboard.rs index a9ee1e8..2a3820e 100644 --- a/challenges/src/endpoints/leaderboard.rs +++ b/challenges/src/endpoints/leaderboard.rs @@ -10,6 +10,7 @@ use poem_openapi::{ }; use schemas::challenges::leaderboard::{Leaderboard, Rank}; use uuid::Uuid; +use chrono::NaiveDateTime; use super::Tags; use crate::services::leaderboard::{ @@ -23,6 +24,34 @@ pub struct LeaderboardEndpoints { pub cache: Cache, } +fn get_current_quarter_range() -> (NaiveDateTime, NaiveDateTime) { + let now = Local::now(); + let year = now.year(); + let quarter = (now.month() - 1) / 3; + + let start_month = quarter * 3 + 1; + let end_month = start_month + 3; + + let start = NaiveDateTime::new( + NaiveDate::from_ymd_opt(year, start_month, 1).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ); + + let end = if end_month > 12 { + NaiveDateTime::new( + NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ) + } else { + NaiveDateTime::new( + NaiveDate::from_ymd_opt(year, end_month, 1).unwrap(), + NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + ) + }; + + (start, end) +} + #[OpenApi(tag = "Tags::Leaderboard")] impl LeaderboardEndpoints { #[oai(path = "/leaderboard", method = "get")] @@ -30,18 +59,36 @@ impl LeaderboardEndpoints { &self, #[oai(validator(maximum(value = "100")))] limit: Query, offset: Query, + current_quarter: Query>, _auth: VerifiedUserAuth, ) -> GetLeaderboard::Response { - GetLeaderboard::ok(get_global_leaderboard(&self.state.services, limit.0, offset.0).await?) + let date_range = if current_quarter.0.unwrap_or(false) { + Some(get_current_quarter_range()) + } else { + None + }; + + GetLeaderboard::ok( + get_global_leaderboard(&self.state.services, limit.0, offset.0, date_range).await? + ) } #[oai(path = "/leaderboard/:user_id", method = "get")] async fn get_leaderboard_user( &self, user_id: Query, + current_quarter: Query>, _auth: VerifiedUserAuth, ) -> GetLeaderboardUser::Response { - GetLeaderboardUser::ok(get_global_leaderboard_user(&self.state.services, user_id.0).await?) + let date_range = if current_quarter.0.unwrap_or(false) { + Some(get_current_quarter_range()) + } else { + None + }; + + GetLeaderboardUser::ok( + get_global_leaderboard_user(&self.state.services, user_id.0, date_range).await? + ) } #[oai(path = "/leaderboard/by-task/:task_id", method = "get")] @@ -50,16 +97,23 @@ impl LeaderboardEndpoints { task_id: Path, #[oai(validator(maximum(value = "100")))] limit: Query, offset: Query, + current_quarter: Query>, db: Data<&DbTxn>, _auth: VerifiedUserAuth, ) -> GetTaskLeaderboard::Response { + let date_range = if current_quarter.0.unwrap_or(false) { + Some(get_current_quarter_range()) + } else { + None + }; + let leaderboard = self .cache .cached_result( - key!(task_id.0, limit.0, offset.0), + key!(task_id.0, limit.0, offset.0, current_quarter.0), &[], Some(Duration::from_secs(10)), - || get_task_leaderboard(&db, &self.state.services, task_id.0, limit.0, offset.0), + || get_task_leaderboard(&db, &self.state.services, task_id.0, limit.0, offset.0, date_range), ) .await??; GetTaskLeaderboard::ok(leaderboard) @@ -70,16 +124,23 @@ impl LeaderboardEndpoints { &self, task_id: Path, user_id: Path, + current_quarter: Query>, db: Data<&DbTxn>, _auth: VerifiedUserAuth, ) -> GetTaskLeaderboardUser::Response { + let date_range = if current_quarter.0.unwrap_or(false) { + Some(get_current_quarter_range()) + } else { + None + }; + let rank = self .cache .cached_result( - key!(task_id.0, user_id.0), + key!(task_id.0, user_id.0, current_quarter.0), &[], Some(Duration::from_secs(10)), - || get_task_leaderboard_user(&db, task_id.0, user_id.0), + || get_task_leaderboard_user(&db, task_id.0, user_id.0, date_range), ) .await??; GetTaskLeaderboardUser::ok(rank) @@ -91,13 +152,20 @@ impl LeaderboardEndpoints { language: Path, #[oai(validator(maximum(value = "100")))] limit: Query, offset: Query, + current_quarter: Query>, db: Data<&DbTxn>, _auth: VerifiedUserAuth, ) -> GetLanguageLeaderboard::Response { + let date_range = if current_quarter.0.unwrap_or(false) { + Some(get_current_quarter_range()) + } else { + None + }; + let leaderboard = self .cache .cached_result( - key!(&language.0, limit.0, offset.0), + key!(&language.0, limit.0, offset.0, current_quarter.0), &[], Some(Duration::from_secs(10)), || { @@ -106,7 +174,8 @@ impl LeaderboardEndpoints { &self.state.services, &language.0, limit.0, - offset.0, + offset.0, + date_range ) }, ) @@ -119,16 +188,23 @@ impl LeaderboardEndpoints { &self, language: Path, user_id: Path, + current_quarter: Query>, db: Data<&DbTxn>, _auth: VerifiedUserAuth, ) -> GetLanguageLeaderboardUser::Response { + let date_range = if current_quarter.0.unwrap_or(false) { + Some(get_current_quarter_range()) + } else { + None + }; + let rank = self .cache .cached_result( - key!(&language.0, user_id.0), + key!(&language.0, user_id.0, current_quarter.0), &[], Some(Duration::from_secs(10)), - || get_language_leaderboard_user(&db, &language.0, user_id.0), + || get_language_leaderboard_user(&db, &language.0, user_id.0, date_range), ) .await??; GetLanguageLeaderboardUser::ok(rank) diff --git a/challenges/src/services/leaderboard/global.rs b/challenges/src/services/leaderboard/global.rs index 9f8f7ea..0ac021c 100644 --- a/challenges/src/services/leaderboard/global.rs +++ b/challenges/src/services/leaderboard/global.rs @@ -2,15 +2,29 @@ use futures::future::try_join_all; use lib::services::Services; use schemas::challenges::leaderboard::{Leaderboard, Rank}; use uuid::Uuid; +use chrono::NaiveDateTime; use super::resolve_user; +/// Get the global leaderboard with optional date range filtering pub async fn get_global_leaderboard( services: &Services, limit: u64, offset: u64, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, ) -> anyhow::Result { - let leaderboard = services.skills.get_leaderboard(limit, offset).await?; + let leaderboard = match date_range { + Some((start_date, end_date)) => { + services.skills + .get_leaderboard_with_date_range(limit, offset, start_date, end_date) + .await? + } + None => { + services.skills + .get_leaderboard(limit, offset) + .await? + } + }; Ok(Leaderboard { leaderboard: try_join_all( @@ -24,9 +38,24 @@ pub async fn get_global_leaderboard( }) } +/// Get a specific user's global rank with optional date range filtering pub async fn get_global_leaderboard_user( services: &Services, user_id: Uuid, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, ) -> anyhow::Result { - Ok(services.skills.get_leaderboard_user(user_id).await?.into()) + let rank = match date_range { + Some((start_date, end_date)) => { + services.skills + .get_leaderboard_user_with_date_range(user_id, start_date, end_date) + .await? + } + None => { + services.skills + .get_leaderboard_user(user_id) + .await? + } + }; + + Ok(rank.into()) } diff --git a/challenges/src/services/leaderboard/language.rs b/challenges/src/services/leaderboard/language.rs index 98d3a15..a6e4c13 100644 --- a/challenges/src/services/leaderboard/language.rs +++ b/challenges/src/services/leaderboard/language.rs @@ -9,10 +9,88 @@ use sea_orm::{ ColumnTrait, DatabaseTransaction, Iden, Value, }; use uuid::Uuid; +use chrono::NaiveDateTime; use super::{get_leaderboard, get_leaderboard_user}; -fn get_base_query(language: &str) -> SelectStatement { +fn get_base_query( + language: &str, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, +) -> SelectStatement { + let mut inner_query = Query::select() + .expr_as( + Expr::col(( + challenges_coding_challenge_submissions::Entity, + challenges_coding_challenge_submissions::Column::Creator, + )), + Alias::new("user_id"), + ) + .expr_as( + Expr::col(challenges_coding_challenge_submissions::Column::SubtaskId), + Alias::new("subtask_id"), + ) + .expr_as( + Expr::col(( + challenges_coding_challenge_submissions::Entity, + challenges_coding_challenge_submissions::Column::CreationTimestamp, + )) + .max(), + Alias::new("last_update"), + ) + .from(challenges_coding_challenge_result::Entity) + .inner_join( + challenges_coding_challenge_submissions::Entity, + Expr::col(( + challenges_coding_challenge_result::Entity, + challenges_coding_challenge_result::Column::SubmissionId, + )) + .equals(( + challenges_coding_challenge_submissions::Entity, + challenges_coding_challenge_submissions::Column::Id, + )), + ) + .inner_join( + challenges_subtasks::Entity, + Expr::col((challenges_subtasks::Entity, challenges_subtasks::Column::Id)) + .equals(( + challenges_coding_challenge_submissions::Entity, + challenges_coding_challenge_submissions::Column::SubtaskId, + )), + ) + .and_where( + Expr::col(challenges_coding_challenge_submissions::Column::Environment) + .eq(language), + ) + .and_where( + Expr::col(challenges_coding_challenge_result::Column::Verdict).eq( + SimpleExpr::Constant(Value::String(Some( + ChallengesVerdictVariant::Ok.to_string().into(), + ))), + ), + ); + + // Add date range filter if provided + if let Some((start_date, end_date)) = date_range { + inner_query = inner_query.and_where( + Expr::col(( + challenges_coding_challenge_submissions::Entity, + challenges_coding_challenge_submissions::Column::CreationTimestamp, + )) + .between(start_date, end_date), + ); + } + + inner_query = inner_query.group_by_columns([ + ( + challenges_coding_challenge_submissions::Entity, + challenges_coding_challenge_submissions::Column::Creator, + ), + ( + challenges_coding_challenge_submissions::Entity, + challenges_coding_challenge_submissions::Column::SubtaskId, + ), + ]); + Query::select() .column(Alias::new("user_id")) .expr_as( @@ -25,71 +103,7 @@ fn get_base_query(language: &str) -> SelectStatement { Expr::col(Alias::new("last_update")).max(), Alias::new("last_update"), ) - .from_subquery( - Query::select() - .expr_as( - Expr::col(( - challenges_coding_challenge_submissions::Entity, - challenges_coding_challenge_submissions::Column::Creator, - )), - Alias::new("user_id"), - ) - .expr_as( - Expr::col(challenges_coding_challenge_submissions::Column::SubtaskId), - Alias::new("subtask_id"), - ) - .expr_as( - Expr::col(( - challenges_coding_challenge_submissions::Entity, - challenges_coding_challenge_submissions::Column::CreationTimestamp, - )) - .max(), - Alias::new("last_update"), - ) - .from(challenges_coding_challenge_result::Entity) - .inner_join( - challenges_coding_challenge_submissions::Entity, - Expr::col(( - challenges_coding_challenge_result::Entity, - challenges_coding_challenge_result::Column::SubmissionId, - )) - .equals(( - challenges_coding_challenge_submissions::Entity, - challenges_coding_challenge_submissions::Column::Id, - )), - ) - .inner_join( - challenges_subtasks::Entity, - Expr::col((challenges_subtasks::Entity, challenges_subtasks::Column::Id)) - .equals(( - challenges_coding_challenge_submissions::Entity, - challenges_coding_challenge_submissions::Column::SubtaskId, - )), - ) - .and_where( - Expr::col(challenges_coding_challenge_submissions::Column::Environment) - .eq(language), - ) - .and_where( - Expr::col(challenges_coding_challenge_result::Column::Verdict).eq( - SimpleExpr::Constant(Value::String(Some( - ChallengesVerdictVariant::Ok.to_string().into(), - ))), - ), - ) - .group_by_columns([ - ( - challenges_coding_challenge_submissions::Entity, - challenges_coding_challenge_submissions::Column::Creator, - ), - ( - challenges_coding_challenge_submissions::Entity, - challenges_coding_challenge_submissions::Column::SubtaskId, - ), - ]) - .to_owned(), - Alias::new("x"), - ) + .from_subquery(inner_query, Alias::new("x")) .inner_join( challenges_subtasks::Entity, Expr::col(Alias::new("subtask_id")).equals(challenges_subtasks::Column::Id), @@ -104,8 +118,9 @@ pub async fn get_language_leaderboard( language: &str, limit: u64, offset: u64, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, ) -> anyhow::Result { - let base_query = get_base_query(language); + let base_query = get_base_query(language, date_range); get_leaderboard(db, services, base_query, limit, offset).await } @@ -113,7 +128,8 @@ pub async fn get_language_leaderboard_user( db: &DatabaseTransaction, language: &str, user_id: Uuid, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, ) -> anyhow::Result { - let base_query = get_base_query(language); + let base_query = get_base_query(language, date_range); get_leaderboard_user(db, base_query, user_id).await } diff --git a/challenges/src/services/leaderboard/task.rs b/challenges/src/services/leaderboard/task.rs index 5f831ae..f8a4dab 100644 --- a/challenges/src/services/leaderboard/task.rs +++ b/challenges/src/services/leaderboard/task.rs @@ -6,11 +6,12 @@ use sea_orm::{ ColumnTrait, DatabaseTransaction, }; use uuid::Uuid; +use chrono::NaiveDateTime; use super::{get_leaderboard, get_leaderboard_user}; -fn get_base_query(task_id: Uuid) -> SelectStatement { - Query::select() +fn get_base_query(task_id: Uuid, date_range: Option<(NaiveDateTime, NaiveDateTime)>) -> SelectStatement { + let mut query = Query::select() .column(Alias::new("user_id")) .expr_as( challenges_subtasks::Column::Xp @@ -31,8 +32,16 @@ fn get_base_query(task_id: Uuid) -> SelectStatement { )), ) .and_where(challenges_user_subtasks::Column::SolvedTimestamp.is_not_null()) - .and_where(challenges_subtasks::Column::TaskId.eq(task_id)) - .group_by_col(challenges_user_subtasks::Column::UserId) + .and_where(challenges_subtasks::Column::TaskId.eq(task_id)); + + // Add date range filter if provided + if let Some((start_date, end_date)) = date_range { + query = query.and_where( + challenges_user_subtasks::Column::SolvedTimestamp.between(start_date, end_date) + ); + } + + query.group_by_col(challenges_user_subtasks::Column::UserId) .to_owned() } @@ -42,8 +51,9 @@ pub async fn get_task_leaderboard( task_id: Uuid, limit: u64, offset: u64, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, ) -> anyhow::Result { - let base_query = get_base_query(task_id); + let base_query = get_base_query(task_id, date_range); get_leaderboard(db, services, base_query, limit, offset).await } @@ -51,7 +61,8 @@ pub async fn get_task_leaderboard_user( db: &DatabaseTransaction, task_id: Uuid, user_id: Uuid, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, ) -> anyhow::Result { - let base_query = get_base_query(task_id); + let base_query = get_base_query(task_id, date_range); get_leaderboard_user(db, base_query, user_id).await -} +} \ No newline at end of file diff --git a/lib/src/services/skills.rs b/lib/src/services/skills.rs index e9f8a69..2dfa64a 100644 --- a/lib/src/services/skills.rs +++ b/lib/src/services/skills.rs @@ -5,6 +5,7 @@ use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +use chrono::NaiveDateTime; use super::{Service, ServiceResult}; @@ -92,18 +93,26 @@ impl SkillsService { &self, limit: u64, offset: u64, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, ) -> ServiceResult { + let query = LeaderboardQuery { + limit, + offset, + start_date: date_range.map(|(start, _)| start), + end_date: date_range.map(|(_, end)| end), + }; + Ok(self .0 .json_cache .cached_result( - key!(limit, offset), + key!(limit, offset, date_range), &[], Some(Duration::from_secs(10)), || async { self.0 .get("/leaderboard") - .query(&[("limit", limit), ("offset", offset)]) + .query(&query) .send() .await? .error_for_status()? @@ -114,18 +123,29 @@ impl SkillsService { .await??) } - pub async fn get_leaderboard_user(&self, user_id: Uuid) -> ServiceResult { + pub async fn get_leaderboard_user( + &self, + user_id: Uuid, + date_range: Option<(NaiveDateTime, NaiveDateTime)>, + ) -> ServiceResult { Ok(self .0 .cache .cached_result( - key!(user_id), + key!(user_id, date_range), &[], Some(Duration::from_secs(10)), || async { - self.0 - .get(&format!("/leaderboard/{user_id}")) - .send() + let mut req = self.0.get(&format!("/leaderboard/{user_id}")); + + if let Some((start, end)) = date_range { + req = req.query(&[ + ("start_date", start.to_string()), + ("end_date", end.to_string()), + ]); + } + + req.send() .await? .error_for_status()? .json() From 6c799b1d0109e4ccce2ea375e51c409f5ab48cb3 Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 29 Oct 2024 22:54:27 +0100 Subject: [PATCH 2/5] added leaderboardquery struct --- lib/src/services/skills.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/services/skills.rs b/lib/src/services/skills.rs index 2dfa64a..77bd5c3 100644 --- a/lib/src/services/skills.rs +++ b/lib/src/services/skills.rs @@ -12,6 +12,16 @@ use super::{Service, ServiceResult}; #[derive(Debug, Clone)] pub struct SkillsService(Service); +#[derive(Debug, Serialize)] +struct LeaderboardQuery { + limit: u64, + offset: u64, + #[serde(skip_serializing_if = "Option::is_none")] + start_date: Option, + #[serde(skip_serializing_if = "Option::is_none")] + end_date: Option, +} + impl SkillsService { pub(super) fn new(service: Service) -> Self { Self(service) From 54bfe5a7bcde8ddde782999eb9c7592c0778e8e0 Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 30 Oct 2024 20:10:49 +0100 Subject: [PATCH 3/5] added chrono to cargo.toml --- justfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/justfile b/justfile index e633fd8..529d67a 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,8 @@ alias m := migrate alias e := entity alias b := bacon +set shell := ["powershell.exe", "-c"] + _default: @just --list From ec42aa4e52275dc2fc6804ad89e422ff8640042a Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 30 Oct 2024 20:13:42 +0100 Subject: [PATCH 4/5] Revert "added chrono to cargo.toml" This reverts commit 54bfe5a7bcde8ddde782999eb9c7592c0778e8e0. --- justfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/justfile b/justfile index 529d67a..e633fd8 100644 --- a/justfile +++ b/justfile @@ -6,8 +6,6 @@ alias m := migrate alias e := entity alias b := bacon -set shell := ["powershell.exe", "-c"] - _default: @just --list From 355c4463235074c55da10459d882cf1c6add28cd Mon Sep 17 00:00:00 2001 From: Christoph Date: Wed, 30 Oct 2024 20:15:46 +0100 Subject: [PATCH 5/5] added chrono to cargo.toml --- lib/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index a8bee56..656515e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -23,3 +23,4 @@ thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } uuid = { workspace = true } +chrono = { workspace = true }