switch to new goal system (shot with a flag), add views separate from the models and various tests and new routes for html naviagtion around the page

master
Tait Hoyem 1 year ago
parent ce45ef847e
commit 0d2e4f3574

@ -3,6 +3,8 @@ CREATE TABLE IF NOT EXISTS teams (
id SERIAL PRIMARY KEY NOT NULL,
name VARCHAR(255) NOT NULL,
division INTEGER NOT NULL,
-- possibly add an image
image VARCHAR(255),
CONSTRAINT division_fk
FOREIGN KEY(division)
REFERENCES divisions(id)

@ -1,2 +1,2 @@
-- Add down migration script here
DROP TABLE games;
DROP TABLE IF EXISTS games;

@ -3,6 +3,8 @@ CREATE TABLE IF NOT EXISTS games (
id SERIAL PRIMARY KEY NOT NULL,
-- a possibly null name for the game; this allows there to be special names like "Gold Medal Game"
name VARCHAR(255),
-- what divison is the game a part of (usefl for stats)
division INTEGER NOT NULL,
team_home INTEGER NOT NULL,
team_away INTEGER NOT NULL,
-- home and away teams need to actually be teams
@ -13,5 +15,10 @@ CREATE TABLE IF NOT EXISTS games (
CONSTRAINT team_away_fk
FOREIGN KEY(team_away)
REFERENCES teams(id)
ON DELETE RESTRICT,
-- is divison real
CONSTRAINT division_fk
FOREIGN KEY(division)
REFERENCES divisions(id)
ON DELETE RESTRICT
);

@ -1,27 +1,31 @@
INSERT INTO games
(id, name, team_home, team_away)
(id, name, division, team_home, team_away)
VALUES
(
1,
'Game 1',
1, -- LV/D
1, -- Bullseye
2 -- Seecats
),
(
2,
'Game 2',
1, -- LV/D
1, -- Bullseye
2 -- Seecats
),
(
3,
'Game 3',
1, -- LV/D
1, -- Bullseye
2 -- Seecats
),
(
4,
'Game 4',
1, -- LV/D
1, -- Bullseye
2 -- Seecats
);

@ -75,7 +75,7 @@ VALUES
3,
282,
3918,
1,
null,
null,
true,
true

@ -1,2 +0,0 @@
-- Add down migration script here
DROP FUNCTION IF EXISTS player_stats_overview_all;

@ -1,29 +0,0 @@
-- Add up migration script here
CREATE OR REPLACE FUNCTION player_stats_overview_all() RETURNS VOID
LANGUAGE SQL
AS $$
SELECT
(
SELECT COUNT(id)
FROM shots
WHERE shooter=players.id
AND goal=true
) AS goals,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
AND goal=true
) AS assists,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
OR shooter=players.id
) AS points,
players.name AS player_name
FROM players
ORDER BY
points DESC,
players.name;
$$;

@ -1,5 +1,6 @@
mod db;
mod model;
mod views;
use crate::model::{
TableName,
@ -9,6 +10,11 @@ use crate::model::{
TeamPlayer,
Player,
Shot,
Game,
};
use views::{
get_score_from_game,
get_box_score_from_game,
};
use sqlx::{
@ -24,6 +30,7 @@ use axum::{
},
response::{
Json,
Html,
IntoResponse,
},
routing::get,
@ -44,18 +51,10 @@ async fn main() {
db_pool: Arc::new(pool),
};
let router = Router::new()
.route("/league/", get(league_all))
.route("/league/:id", get(league_id))
.route("/division/", get(division_all))
.route("/division/:id", get(division_id))
.route("/team/", get(team_all))
.route("/team/:id", get(team_id))
.route("/player/", get(player_all))
.route("/player/:id", get(player_id))
.route("/team-player/", get(team_player_all))
.route("/team-playerplayer/:id", get(team_player_id))
.route("/shot/", get(shots_all))
.route("/shot/:id", get(shots_id))
.route("/", get(league_html))
.route("/league/:id/divisions/", get(divisions_for_league_html))
.route("/division/:id/", get(games_for_division_html))
.route("/game/:id/", get(score_for_game_html))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
println!("Listening on {}", addr);
@ -65,7 +64,6 @@ async fn main() {
.unwrap();
}
async fn get_all<T: Send + Unpin + TableName + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool) -> Result<Vec<T>, sqlx::Error> {
sqlx::query_as::<_, T>(
&format!("SELECT * FROM {};", <T as TableName>::TABLE_NAME)
@ -117,6 +115,82 @@ macro_rules! get_by_id {
}
}
async fn league_html(State(server_config): State<ServerState>) -> impl IntoResponse {
let leagues_html = get_all::<League>(&server_config.db_pool).await
.unwrap()
.iter()
.map(|league| {
format!(
"<li><a href=\"{1}\">{0}</a></li>",
league.name,
format!("/league/{}/divisions/", league.id),
)
})
.collect::<Vec<String>>()
.join("\n");
let html = format!("<ul>{leagues_html}</ul>");
(StatusCode::OK, Html(html))
}
async fn divisions_for_league_html(State(server_config): State<ServerState>, Path(league_id): Path<i32>) -> impl IntoResponse {
let leagues_html = sqlx::query_as::<_, Division>("SELECT * FROM divisions WHERE league = $1")
.bind(league_id)
.fetch_all(&*server_config.db_pool)
.await
.unwrap()
.iter()
.map(|division| {
format!(
"<li><a href=\"{1}\">{0}</a></li>",
division.name,
format!("/division/{}/", division.id),
)
})
.collect::<Vec<String>>()
.join("\n");
let html = format!("<ul>{leagues_html}</ul>");
(StatusCode::OK, Html(html))
}
async fn games_for_division_html(State(server_config): State<ServerState>, Path(division_id): Path<i32>) -> impl IntoResponse {
let leagues_html = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE division = $1")
.bind(division_id)
.fetch_all(&*server_config.db_pool)
.await
.unwrap()
.iter()
.map(|game| {
format!(
"<li><a href=\"{1}\">{0}</a></li>",
game.name,
format!("/game/{}/", game.id),
)
})
.collect::<Vec<String>>()
.join("\n");
let html = format!("<ul>{leagues_html}</ul>");
(StatusCode::OK, Html(html))
}
async fn score_for_game_html(State(server_config): State<ServerState>, Path(game_id): Path<i32>) -> impl IntoResponse {
let game = sqlx::query_as::<_, Game>(
"SELECT * FROM games WHERE id = $1;"
)
.bind(game_id)
.fetch_one(&*server_config.db_pool)
.await
.unwrap();
let score = get_score_from_game(&server_config.db_pool, &game).await.unwrap();
let box_score_html = get_box_score_from_game(&server_config.db_pool, &game).await.unwrap()
.iter()
.map(|player_stats| {
format!("<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td></tr>", player_stats.player_name, player_stats.points, player_stats.goals, player_stats.assists)
})
.collect::<Vec<String>>()
.join("");
let html = format!("<p>{}: {}<br>{}: {}</p><table>{}</table>", score.home_name, score.home, score.away_name, score.away, box_score_html);
(StatusCode::OK, Html(html))
}
/*
macro_rules! insert {
($crud_struct:ident, $func_name:ident) => {
@ -129,7 +203,6 @@ macro_rules! insert {
}
}
}
*/
macro_rules! impl_all_query_types {
($ty:ident, $func_all:ident, $func_by_id:ident) => {
@ -169,3 +242,4 @@ impl_all_query_types!(
league_id
);
*/

@ -1,5 +1,4 @@
use sqlx::FromRow;
use sqlx::PgPool;
use sqlx::types::chrono::{DateTime, Utc};
use chrono::serde::{ts_seconds, ts_seconds_option};
use serde::{Serialize, Deserialize};
@ -15,7 +14,7 @@ macro_rules! impl_table_name {
}
}
#[derive(FromRow, Serialize, Deserialize)]
#[derive(FromRow, Serialize, Deserialize, Debug)]
pub struct League {
pub id: i32,
pub name: String,
@ -52,6 +51,7 @@ pub struct Team {
pub id: i32,
pub name: String,
pub division: i32,
pub image: Option<String>,
}
#[derive(FromRow, Serialize, Deserialize)]
@ -95,55 +95,13 @@ pub struct TeamPlayer {
pub position: i32,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct Notification {
pub scorer_name: String,
pub scorer_number: i32,
pub position: String,
pub scorer_team_name: String,
pub period_name: String,
pub period_time_left: i32,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct PlayerStats {
pub player_name: String,
pub goals: i64,
pub assists: i64,
pub points: i64,
}
async fn get_player_stats_overview(pool: PgPool) -> Result<Vec<PlayerStats>, sqlx::Error> {
let query = r#"
SELECT
(
SELECT COUNT(id)
FROM shots
WHERE shooter=players.id
AND goal=true
) AS goals,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
AND goal=true
) AS assists,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
OR shooter=players.id
) AS points,
players.name AS player_name
FROM players
ORDER BY
points DESC,
players.name;
"#;
let result = sqlx::query_as::<_, PlayerStats>(query)
.fetch_all(&pool)
.await;
result
#[derive(FromRow, Deserialize, Serialize)]
pub struct Game {
pub id: i32,
pub division: i32,
pub name: String,
pub team_home: i32,
pub team_away: i32,
}
impl_table_name!(TeamPlayer, "team_players");
@ -152,6 +110,7 @@ impl_table_name!(League, "leagues");
impl_table_name!(Division, "divisions");
impl_table_name!(Team, "teams");
impl_table_name!(Shot, "shots");
impl_table_name!(Game, "games");
#[cfg(test)]
mod tests {
@ -164,59 +123,9 @@ mod tests {
Team,
Shot,
TableName,
Notification,
get_player_stats_overview,
Game,
};
#[test]
fn check_player_overall_stats() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let players_stats = get_player_stats_overview(pool).await.unwrap();
for player_stats in players_stats {
println!("{player_stats:?}");
}
})
}
#[test]
fn check_notification_query() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let query = r#"
SELECT
teams.name AS scorer_team_name,
players.name AS scorer_name,
positions.name AS position,
team_players.player_number AS scorer_number,
shots.period_time AS period_time_left,
periods.name AS period_name
FROM
shots
JOIN teams ON teams.id=shots.shooter_team
JOIN players ON players.id=shots.shooter
JOIN team_players ON team_players.player=players.id AND team_players.team=teams.id
JOIN periods ON periods.id=shots.period
JOIN positions ON positions.id=team_players.position;
"#;
let result = sqlx::query_as::<_, Notification>(query)
.fetch_one(&pool)
.await
.unwrap();
let minutes = result.period_time_left / 60;
let seconds = result.period_time_left % 60;
println!("{0} {1} player #{3} {2} has scored! Time of the goal: {4}:{5} in the {6}",
result.scorer_team_name,
result.position,
result.scorer_name,
result.scorer_number,
minutes,
seconds,
result.period_name
);
});
}
/// A simple function to connect to the database.
async fn db_connect() -> sqlx::PgPool {
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set to run tests.");
@ -258,4 +167,5 @@ JOIN positions ON positions.id=team_players.position;
generate_select_test!(Division, select_division);
generate_select_test!(Team, select_team);
generate_select_test!(Shot, select_shot);
generate_select_test!(Game, select_game);
}

@ -0,0 +1,267 @@
use sqlx::FromRow;
use sqlx::PgPool;
use crate::model::Game;
use serde::{Serialize, Deserialize};
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct Score {
pub home: i64,
pub home_name: String,
pub away: i64,
pub away_name: String,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct Notification {
pub scorer_name: String,
pub scorer_number: i32,
pub position: String,
pub scorer_team_name: String,
pub period_name: String,
pub period_time_left: i32,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct PlayerStats {
pub player_name: String,
pub goals: i64,
pub assists: i64,
pub points: i64,
}
pub async fn get_box_score_from_game(pool: &PgPool, game: &Game) -> Result<Vec<PlayerStats>, sqlx::Error> {
let query = format!(r#"
SELECT
(
SELECT COUNT(id)
FROM shots
WHERE shooter=players.id
AND goal=true
AND game=$1
) AS goals,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
AND goal=true
AND game=$1
) AS assists,
(
SELECT COUNT(id)
FROM shots
WHERE (assistant=players.id
OR shooter=players.id)
AND game=$1
) AS points,
players.name AS player_name
FROM players
JOIN shots ON shots.shooter=players.id OR shots.assistant=players.id
WHERE shots.game = $1
GROUP BY players.id
ORDER BY
points DESC,
goals DESC,
players.name;
"#);
sqlx::query_as::<_, PlayerStats>(&query)
.bind(game.id)
.fetch_all(pool)
.await
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct DetailGoals {
pub player_id: i32,
pub player_name: String,
pub number: i32,
pub team_name: String,
pub team_id: i32,
pub time_remaining: i32,
pub period_short_name: String,
pub first_assist_name: Option<String>,
pub first_assist_number: Option<i32>,
pub first_assist_id: Option<i32>,
pub second_assist_name: Option<String>,
pub second_assist_id: Option<i32>,
pub second_assist_number: Option<i32>,
}
pub async fn get_goals_from_game(pool: &PgPool, game: &Game) -> Result<Vec<NiceGoals>, sqlx::Error> {
}
pub async fn get_score_from_game(pool: &PgPool, game: &Game) -> Result<Score, sqlx::Error> {
let query = format!(r#"
SELECT
(
SELECT COUNT(id)
FROM shots
WHERE game=$1
AND goal=true
AND shooter_team=$2
) AS home,
(
SELECT COUNT(id)
FROM shots
WHERE game=$1
AND goal=true
AND shooter_team=$3
) AS away,
(
SELECT name
FROM teams
WHERE id=$2
) AS home_name,
(
SELECT name
FROM teams
WHERE id=$3
) AS away_name
FROM games;
"#);
sqlx::query_as::<_, Score>(&query)
.bind(game.id)
.bind(game.team_home)
.bind(game.team_away)
.fetch_one(pool)
.await
}
async fn get_player_stats_overview(pool: PgPool) -> Result<Vec<PlayerStats>, sqlx::Error> {
let query = r#"
SELECT
(
SELECT COUNT(id)
FROM shots
WHERE shooter=players.id
AND goal=true
) AS goals,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
AND goal=true
) AS assists,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
OR shooter=players.id
) AS points,
players.name AS player_name
FROM players
ORDER BY
points DESC,
goals DESC,
players.name;
"#;
let result = sqlx::query_as::<_, PlayerStats>(query)
.fetch_all(&pool)
.await;
result
}
#[cfg(test)]
mod tests {
use std::env;
use crate::model::{
Game,
};
use crate::views::{
Notification,
get_player_stats_overview,
get_score_from_game,
get_box_score_from_game,
};
#[test]
fn check_box_score_from_game() {
tokio_test::block_on(async move{
let pool = db_connect().await;
let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id=1;")
.fetch_one(&pool)
.await
.unwrap();
let scores = get_box_score_from_game(&pool, &game)
.await
.unwrap();
println!("{scores:?}");
assert_eq!(scores.get(0).unwrap().player_name, "Brian MacLean");
})
}
#[test]
fn check_game_score() {
tokio_test::block_on(async move{
let pool = db_connect().await;
let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id=1;")
.fetch_one(&pool)
.await
.unwrap();
let score = get_score_from_game(&pool, &game)
.await
.unwrap();
assert_eq!(score.away, 1);
assert_eq!(score.home, 1);
})
}
#[test]
fn check_player_overall_stats() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let players_stats = get_player_stats_overview(pool).await.unwrap();
for player_stats in players_stats {
println!("{player_stats:?}");
}
})
}
#[test]
fn check_notification_query() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let query = r#"
SELECT
teams.name AS scorer_team_name,
players.name AS scorer_name,
positions.name AS position,
team_players.player_number AS scorer_number,
shots.period_time AS period_time_left,
periods.name AS period_name
FROM
shots
JOIN teams ON teams.id=shots.shooter_team
JOIN players ON players.id=shots.shooter
JOIN team_players ON team_players.player=players.id AND team_players.team=teams.id
JOIN periods ON periods.id=shots.period
JOIN positions ON positions.id=team_players.position;
"#;
let result = sqlx::query_as::<_, Notification>(query)
.fetch_one(&pool)
.await
.unwrap();
let minutes = result.period_time_left / 60;
let seconds = result.period_time_left % 60;
println!("{0} {1} player #{3} {2} has scored! Time of the goal: {4}:{5} in the {6}",
result.scorer_team_name,
result.position,
result.scorer_name,
result.scorer_number,
minutes,
seconds,
result.period_name
);
});
}
/// A simple function to connect to the database.
async fn db_connect() -> sqlx::PgPool {
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set to run tests.");
sqlx::postgres::PgPoolOptions::new()
.max_connections(1)
.connect(&db_url)
.await
.expect("Active database connection must be made")
}
}
Loading…
Cancel
Save