From 1e6a1e2c1a3b362cabb24af1413b785463434c7b Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Thu, 9 Jan 2025 06:04:16 +0530 Subject: [PATCH 1/5] clean up schededule_task_at_midnight()'s logic --- src/main.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index ad07d03..e8e5e64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,27 +62,29 @@ async fn main( ) .with_state(state) .layer(cors); + task::spawn(async move { - schedule_task_at_midnight(pool.clone()).await; // Call the function after 10 seconds + schedule_task_at_midnight(pool.clone()).await; }); + Ok(router.into()) } -//Ticker for calling the scheduled task +// Sleep till midnight, then execute the task, repeat. async fn schedule_task_at_midnight(pool: Arc) { loop { let now = Local::now(); + let next_midnight = (now + chrono::Duration::days(1)) + .date_naive() + .and_hms_opt(0, 0, 0) + .unwrap(); - let tomorrow = now.date_naive().succ_opt().unwrap(); - let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); - let next_midnight = tomorrow.and_time(midnight); - - let now_naive = now.naive_local(); - let duration_until_midnight = next_midnight.signed_duration_since(now_naive); - let sleep_duration = Duration::from_secs(duration_until_midnight.num_seconds() as u64 + 60); + let duration_until_midnight = next_midnight.signed_duration_since(now.naive_local()); + let sleep_duration = tokio::time::Duration::from_secs(duration_until_midnight.num_seconds() as u64); sleep_until(Instant::now() + sleep_duration).await; scheduled_task(pool.clone()).await; + // TODO: Use tracing print!("done"); } } From 077d8f15bad1551292508ac3a80b50dadd603ce3 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Thu, 9 Jan 2025 06:05:54 +0530 Subject: [PATCH 2/5] add todos for future work --- src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index e8e5e64..5b9317f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,18 +25,18 @@ struct MyState { secret_key: String, } -//Main method #[shuttle_runtime::main] async fn main( #[shuttle_shared_db::Postgres] pool: PgPool, #[shuttle_runtime::Secrets] secrets: SecretStore, ) -> shuttle_axum::ShuttleAxum { + // TODO: Explain? env::set_var("PGOPTIONS", "-c ignore_version=true"); sqlx::migrate!() .run(&pool) .await - .expect("Failed to run migrations"); + .expect("Failed to run migrations."); let pool = Arc::new(pool); let secret_key = secrets.get("ROOT_SECRET").expect("ROOT_SECRET not found"); @@ -50,6 +50,7 @@ async fn main( secret_key: secret_key.clone(), }; + // TODO: Restrict to amD and Home let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(tower_http::cors::Any) From c45f36aa9e69297db8c091ddb489d93929dc2ea2 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Thu, 9 Jan 2025 06:09:36 +0530 Subject: [PATCH 3/5] run cargo fmt --- src/attendance/scheduled_task.rs | 16 ++- src/db/attendance.rs | 10 +- src/db/projects.rs | 4 +- src/graphql/mutations.rs | 152 ++++++++++++++------------ src/graphql/query.rs | 104 +++++++++--------- src/leaderboard/mod.rs | 2 - src/leaderboard/update_leaderboard.rs | 5 +- src/lib.rs | 4 +- src/routes.rs | 3 +- 9 files changed, 162 insertions(+), 138 deletions(-) diff --git a/src/attendance/scheduled_task.rs b/src/attendance/scheduled_task.rs index a49c8a4..efceb51 100644 --- a/src/attendance/scheduled_task.rs +++ b/src/attendance/scheduled_task.rs @@ -57,7 +57,7 @@ pub async fn scheduled_task(pool: Arc) { if let Ok(Some(leetcode_stats)) = leetcode_username { let username = leetcode_stats.leetcode_username.clone(); - + // Fetch and update LeetCode stats match fetch_leetcode_stats(pool.clone(), member.id, &username).await { Ok(_) => println!("LeetCode stats updated for member ID: {}", member.id), @@ -104,8 +104,13 @@ pub async fn scheduled_task(pool: Arc) { // Function to update attendance streak async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { - let today = chrono::Local::now().with_timezone(&chrono_tz::Asia::Kolkata).naive_local(); - let yesterday = today.checked_sub_signed(chrono::Duration::hours(12)).unwrap().date(); + let today = chrono::Local::now() + .with_timezone(&chrono_tz::Asia::Kolkata) + .naive_local(); + let yesterday = today + .checked_sub_signed(chrono::Duration::hours(12)) + .unwrap() + .date(); if today.day() == 1 { let _ = sqlx::query( @@ -176,6 +181,9 @@ async fn update_attendance_streak(member_id: i32, pool: &sqlx::PgPool) { println!("Sreak not incremented for member ID: {}", member_id); } Ok(_) => eprintln!("Unexpected attendance value for member ID: {}", member_id), - Err(e) => eprintln!("Error checking attendance for member ID {}: {:?}", member_id, e), + Err(e) => eprintln!( + "Error checking attendance for member ID {}: {:?}", + member_id, e + ), } } diff --git a/src/db/attendance.rs b/src/db/attendance.rs index f03da67..4aa7f77 100644 --- a/src/db/attendance.rs +++ b/src/db/attendance.rs @@ -1,6 +1,6 @@ +use async_graphql::SimpleObject; use chrono::{NaiveDate, NaiveTime}; use sqlx::FromRow; -use async_graphql::SimpleObject; //Struct for the Attendance table #[derive(FromRow, SimpleObject)] @@ -22,19 +22,19 @@ pub struct AttendanceStreak { #[derive(FromRow, SimpleObject)] pub struct AttendanceSummary { - pub max_days:i64, + pub max_days: i64, pub member_attendance: Vec, pub daily_count: Vec, } #[derive(FromRow, SimpleObject)] pub struct MemberAttendance { - pub id:i32, - pub present_days:i64, + pub id: i32, + pub present_days: i64, } #[derive(FromRow, SimpleObject)] pub struct DailyCount { pub date: NaiveDate, pub count: i64, -} \ No newline at end of file +} diff --git a/src/db/projects.rs b/src/db/projects.rs index d0f930f..448c5e5 100644 --- a/src/db/projects.rs +++ b/src/db/projects.rs @@ -1,9 +1,9 @@ -use sqlx::FromRow; use async_graphql::SimpleObject; +use sqlx::FromRow; #[derive(FromRow, SimpleObject)] pub struct ActiveProjects { id: i32, member_id: i32, project_title: Option, -} \ No newline at end of file +} diff --git a/src/graphql/mutations.rs b/src/graphql/mutations.rs index 74a0d55..7a13211 100644 --- a/src/graphql/mutations.rs +++ b/src/graphql/mutations.rs @@ -1,41 +1,44 @@ -use async_graphql::{Context, Object}; use ::chrono::Local; +use async_graphql::{Context, Object}; use chrono::{NaiveDate, NaiveTime}; use chrono_tz::Asia::Kolkata; -use sqlx::PgPool; +use hmac::{Hmac, Mac}; +use sha2::Sha256; use sqlx::types::chrono; +use sqlx::PgPool; use std::sync::Arc; -use hmac::{Hmac,Mac}; -use sha2::Sha256; - type HmacSha256 = Hmac; -use crate::db::{attendance::Attendance, leaderboard::{CodeforcesStats, LeetCodeStats}, member::Member, member::StreakUpdate, projects::ActiveProjects}; +use crate::db::{ + attendance::Attendance, + leaderboard::{CodeforcesStats, LeetCodeStats}, + member::Member, + member::StreakUpdate, + projects::ActiveProjects, +}; pub struct MutationRoot; #[Object] impl MutationRoot { - //Mutation for adding members to the Member table async fn add_member( - &self, - ctx: &Context<'_>, - rollno: String, - name: String, - hostel: String, - email: String, - sex: String, + &self, + ctx: &Context<'_>, + rollno: String, + name: String, + hostel: String, + email: String, + sex: String, year: i32, macaddress: String, discord_id: String, group_id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - - + let pool = ctx + .data::>() + .expect("Pool not found in context"); let member = sqlx::query_as::<_, Member>( "INSERT INTO Member (rollno, name, hostel, email, sex, year, macaddress, discord_id, group_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *" @@ -55,7 +58,6 @@ impl MutationRoot { Ok(member) } - async fn edit_member( &self, ctx: &Context<'_>, @@ -66,25 +68,31 @@ impl MutationRoot { discord_id: String, group_id: i32, hmac_signature: String, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - - let secret_key = ctx.data::().expect("HMAC secret not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); + + let secret_key = ctx + .data::() + .expect("HMAC secret not found in context"); - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); + let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) + .expect("HMAC can take key of any size"); - let message = format!("{}{}{}{}{}{}", id, hostel, year, macaddress, discord_id, group_id); + let message = format!( + "{}{}{}{}{}{}", + id, hostel, year, macaddress, discord_id, group_id + ); mac.update(message.as_bytes()); let expected_signature = mac.finalize().into_bytes(); - + // Convert the received HMAC signature from the client to bytes for comparison let received_signature = hex::decode(hmac_signature) .map_err(|_| sqlx::Error::Protocol("Invalid HMAC signature".into()))?; - if expected_signature.as_slice() != received_signature.as_slice() { - return Err(sqlx::Error::Protocol("HMAC verification failed".into())); } @@ -99,9 +107,8 @@ impl MutationRoot { group_id = CASE WHEN $5 = 0 THEN group_id ELSE $5 END WHERE id = $6 RETURNING * - " + ", ) - .bind(hostel) .bind(year) .bind(macaddress) @@ -113,27 +120,25 @@ impl MutationRoot { Ok(member) } - + //Mutation for adding attendance to the Attendance table async fn add_attendance( - &self, - + ctx: &Context<'_>, id: i32, date: NaiveDate, timein: NaiveTime, timeout: NaiveTime, is_present: bool, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); - + let pool = ctx + .data::>() + .expect("Pool not found in context"); let attendance = sqlx::query_as::<_, Attendance>( "INSERT INTO Attendance (id, date, timein, timeout, is_present) VALUES ($1, $2, $3, $4, $5) RETURNING *" ) - .bind(id) .bind(date) .bind(timein) @@ -144,34 +149,36 @@ impl MutationRoot { Ok(attendance) } - + async fn mark_attendance( &self, ctx: &Context<'_>, id: i32, date: NaiveDate, is_present: bool, - hmac_signature: String, - ) -> Result { - - let pool = ctx.data::>().expect("Pool not found in context"); + hmac_signature: String, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let secret_key = ctx.data::().expect("HMAC secret not found in context"); + let secret_key = ctx + .data::() + .expect("HMAC secret not found in context"); - let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()).expect("HMAC can take key of any size"); + let mut mac = HmacSha256::new_from_slice(secret_key.as_bytes()) + .expect("HMAC can take key of any size"); let message = format!("{}{}{}", id, date, is_present); mac.update(message.as_bytes()); let expected_signature = mac.finalize().into_bytes(); - + // Convert the received HMAC signature from the client to bytes for comparison let received_signature = hex::decode(hmac_signature) .map_err(|_| sqlx::Error::Protocol("Invalid HMAC signature".into()))?; - if expected_signature.as_slice() != received_signature.as_slice() { - return Err(sqlx::Error::Protocol("HMAC verification failed".into())); } @@ -186,7 +193,7 @@ impl MutationRoot { is_present = $2 WHERE id = $3 AND date = $4 RETURNING * - " + ", ) .bind(current_time) .bind(is_present) @@ -199,14 +206,16 @@ impl MutationRoot { } //here when user changes the handle, it just updates the handle in the database without updating the other values till midnight - + async fn add_or_update_leetcode_username( &self, ctx: &Context<'_>, member_id: i32, username: String, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let result = sqlx::query_as::<_, LeetCodeStats>( " @@ -231,7 +240,9 @@ impl MutationRoot { member_id: i32, handle: String, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let result = sqlx::query_as::<_, CodeforcesStats>( " @@ -255,20 +266,22 @@ impl MutationRoot { id: i32, has_sent_update: bool, ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak_info = sqlx::query_as::<_, StreakUpdate>( " SELECT id, streak, max_streak FROM StreakUpdate WHERE id = $1 - " + ", ) .bind(id) .fetch_optional(pool.as_ref()) .await?; - match streak_info{ + match streak_info { Some(mut member) => { let current_streak = member.streak.unwrap_or(0); let max_streak = member.max_streak.unwrap_or(0); @@ -285,7 +298,7 @@ impl MutationRoot { SET streak = $1, max_streak = $2 WHERE id = $3 RETURNING * - " + ", ) .bind(new_streak) .bind(new_max_streak) @@ -294,14 +307,14 @@ impl MutationRoot { .await?; Ok(updated_member) - }, + } None => { let new_member = sqlx::query_as::<_, StreakUpdate>( " INSERT INTO StreakUpdate (id, streak, max_streak) VALUES ($1, $2, $3) RETURNING * - " + ", ) .bind(id) .bind(0) @@ -318,16 +331,18 @@ impl MutationRoot { &self, ctx: &Context<'_>, id: i32, - project_name:String, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + project_name: String, + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let active_project = sqlx::query_as::<_,ActiveProjects>( + let active_project = sqlx::query_as::<_, ActiveProjects>( " INSERT INTO ActiveProjects (member_id,project_title) VALUES ($1,$2) RETURNING * - " + ", ) .bind(id) .bind(project_name) @@ -341,15 +356,17 @@ impl MutationRoot { &self, ctx: &Context<'_>, project_id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); - let active_project = sqlx::query_as::<_,ActiveProjects>( + let active_project = sqlx::query_as::<_, ActiveProjects>( " DELETE FROM ActiveProjects WHERE id = $1 RETURNING * - " + ", ) .bind(project_id) .fetch_one(pool.as_ref()) @@ -357,5 +374,4 @@ impl MutationRoot { Ok(active_project) } - -} \ No newline at end of file +} diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 65c5d76..0d1a896 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -1,11 +1,16 @@ +use crate::db::{ + attendance::Attendance, + leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, + member::{Member, StreakUpdate}, +}; use async_graphql::{Context, Object}; use chrono::NaiveDate; -use root::db::{attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, projects::ActiveProjects}; +use root::db::{ + attendance::{AttendanceStreak, AttendanceSummary, DailyCount, MemberAttendance}, + projects::ActiveProjects, +}; use sqlx::PgPool; use std::sync::Arc; -use crate::db::{ - attendance::Attendance, leaderboard::{CodeforcesStatsWithName, LeaderboardWithMember, LeetCodeStatsWithName}, member::{Member, StreakUpdate} -}; pub struct QueryRoot; @@ -97,43 +102,42 @@ impl QueryRoot { .await?; Ok(attendance_list) } - async fn get_streak( - &self, - ctx: &Context<'_>, - id: i32, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + async fn get_streak(&self, ctx: &Context<'_>, id: i32) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate WHERE id = $1") - .bind(id) - .fetch_one(pool.as_ref()) - .await?; + .bind(id) + .fetch_one(pool.as_ref()) + .await?; Ok(streak) } - async fn get_update_streak( - &self, - ctx: &Context<'_>, - ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); + async fn get_update_streak(&self, ctx: &Context<'_>) -> Result, sqlx::Error> { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let streak = sqlx::query_as::<_, StreakUpdate>("SELECT * FROM StreakUpdate") - .fetch_all(pool.as_ref()) - .await?; + .fetch_all(pool.as_ref()) + .await?; Ok(streak) } - + async fn get_attendance_streak( &self, ctx: &Context<'_>, start_date: NaiveDate, end_date: NaiveDate, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - let attendance_streak = sqlx::query_as::<_,AttendanceStreak>( + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let attendance_streak = sqlx::query_as::<_, AttendanceStreak>( "SELECT * from AttendanceStreak WHERE month >= $1 AND month < $2 - " + ", ) .bind(start_date) .bind(end_date) @@ -148,13 +152,15 @@ impl QueryRoot { ctx: &Context<'_>, start_date: NaiveDate, end_date: NaiveDate, - ) -> Result { - let pool = ctx.data::>().expect("Pool not found in context"); + ) -> Result { + let pool = ctx + .data::>() + .expect("Pool not found in context"); let attendance_days = sqlx::query_as::<_, (NaiveDate, i64)>( "SELECT date, COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 AND is_present = true - GROUP BY date ORDER BY date" + GROUP BY date ORDER BY date", ) .bind(start_date) .bind(end_date) @@ -165,7 +171,7 @@ impl QueryRoot { "SELECT id, COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 AND is_present = true - GROUP BY id" + GROUP BY id", ) .bind(start_date) .bind(end_date) @@ -175,7 +181,7 @@ impl QueryRoot { let max_count = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM Attendance WHERE date >= $1 AND date < $2 - AND is_present = true" + AND is_present = true", ) .bind(start_date) .bind(end_date) @@ -183,18 +189,16 @@ impl QueryRoot { .await?; let daily_count = attendance_days - .into_iter().map(|(date, count)| DailyCount{ - date, count - }) + .into_iter() + .map(|(date, count)| DailyCount { date, count }) .collect(); let member_attendance = member_attendance - .into_iter().map(|(id, present_days)| MemberAttendance{ - id, present_days - }) + .into_iter() + .map(|(id, present_days)| MemberAttendance { id, present_days }) .collect(); - let summaries: AttendanceSummary = AttendanceSummary{ + let summaries: AttendanceSummary = AttendanceSummary { max_days: max_count[0], member_attendance, daily_count, @@ -207,33 +211,35 @@ impl QueryRoot { &self, ctx: &Context<'_>, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - + let pool = ctx + .data::>() + .expect("Pool not found in context"); + let dates = sqlx::query_scalar::<_, NaiveDate>( "SELECT date FROM Attendance GROUP BY date HAVING BOOL_AND(NOT is_present) - ORDER BY date" + ORDER BY date", ) .fetch_all(pool.as_ref()) .await?; - + Ok(dates) } - + pub async fn get_projects( &self, ctx: &Context<'_>, ) -> Result, sqlx::Error> { - let pool = ctx.data::>().expect("Pool not found in context"); - - let active_projects = sqlx::query_as::<_, ActiveProjects>( - "SELECT * FROM ActiveProjects" - ) - .fetch_all(pool.as_ref()) - .await?; - + let pool = ctx + .data::>() + .expect("Pool not found in context"); + + let active_projects = sqlx::query_as::<_, ActiveProjects>("SELECT * FROM ActiveProjects") + .fetch_all(pool.as_ref()) + .await?; + Ok(active_projects) } } diff --git a/src/leaderboard/mod.rs b/src/leaderboard/mod.rs index 7d7eadf..5dbae36 100644 --- a/src/leaderboard/mod.rs +++ b/src/leaderboard/mod.rs @@ -1,4 +1,2 @@ pub mod fetch_stats; pub mod update_leaderboard; - - diff --git a/src/leaderboard/update_leaderboard.rs b/src/leaderboard/update_leaderboard.rs index e8bf348..f575957 100644 --- a/src/leaderboard/update_leaderboard.rs +++ b/src/leaderboard/update_leaderboard.rs @@ -1,8 +1,7 @@ -use std::sync::Arc; use sqlx::PgPool; +use std::sync::Arc; pub async fn update_leaderboard(pool: Arc) -> Result<(), Box> { - let leetcode_stats: Result, _> = sqlx::query_as::<_, (i32, i32, i32, i32, i32, i32, i32, i32)>( "SELECT id, member_id, problems_solved, easy_solved, medium_solved, hard_solved, contests_participated, best_rank @@ -19,7 +18,6 @@ pub async fn update_leaderboard(pool: Arc) -> Result<(), Box, _> = sqlx::query_as::<_, (i32, i32, i32, i32, i32)>( "SELECT id, member_id, codeforces_rating, max_rating, contests_participated @@ -49,7 +47,6 @@ pub async fn update_leaderboard(pool: Arc) -> Result<(), Box impl IntoResponse { Html( From 5c0b0db31d67999482fca3522ce2124974ff8c7b Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Thu, 9 Jan 2025 06:12:16 +0530 Subject: [PATCH 4/5] remove unused imports --- src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5b9317f..a56160d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,10 @@ use crate::routes::graphiql; use async_graphql::{EmptySubscription, Schema}; use async_graphql_axum::GraphQL; use axum::{routing::get, Router}; -use chrono::{Local, NaiveTime}; +use chrono::Local; use root::attendance::scheduled_task::scheduled_task; use shuttle_runtime::SecretStore; use sqlx::PgPool; -use std::time::Duration; use std::{env, sync::Arc}; use tokio::task; use tokio::time::{sleep_until, Instant}; From c350dfc083a4bbc0d41742eb15d59355a5fd6000 Mon Sep 17 00:00:00 2001 From: Ivin Joel Abraham Date: Thu, 9 Jan 2025 06:12:48 +0530 Subject: [PATCH 5/5] clean up comment in src/lib.rs --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f549c81..658bc05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -//need this to expose db module for tests +// Need this to expose DB module for tests pub mod attendance; pub mod db; pub mod leaderboard;