Add IIHF stats/points functions, integrate into main, add tests

master
Tait Hoyem 1 year ago
parent 4019823dd7
commit 4b1e7d71ae

@ -1,2 +0,0 @@
-- Add down migration script here
--DROP FUNCTION game_results(INTEGER, INTEGER);

@ -1,99 +0,0 @@
-- Add up migration script here
--CREATE FUNCTION periods(game_id INTEGER)
--RETURNS INTEGER AS $$
--BEGIN
-- RETURN (SELECT COUNT(id) FROM periods WHERE periods.game=game_id);
--END;
--$$ LANGUAGE plpgsql;
--
--CREATE FUNCTION goals(game_id INTEGER, team_id INTEGER)
--RETURNS INTEGER AS $$
--DECLARE
-- goals INTEGER;
--BEGIN
-- IF NOT EXISTS (SELECT * FROM games WHERE games.id=game_id) THEN
-- RAISE EXCEPTION 'The game does not exist.';
-- END IF;
-- IF NOT EXISTS (SELECT * FROM teams WHERE teams.id=team_id) THEN
-- RAISE EXCEPTION 'The team does not exist.';
-- END IF;
-- IF NOT EXISTS (SELECT * FROM games JOIN teams ON teams.id=games.team_home OR teams.id=team_away WHERE games.id=game_id) THEN
-- RAISE EXCEPTION 'The team specified did not play this game.';
-- END IF;
--
-- SELECT
-- COUNT(shots.id)
-- INTO
-- goals
-- FROM shots
-- JOIN game_players
-- ON game_players.id=shots.shooter
-- JOIN periods
-- ON periods.id=shots.period
-- WHERE shots.goal=true
-- AND game_players.team=team_id
-- AND periods.game=game_id;
-- -- return 0 if not goals are found given the team and the game
-- RETURN COALESCE(goals, 0);
--END;
--$$ LANGUAGE plpgsql;
--
--CREATE OR REPLACE FUNCTION calculate_iihf_stats(game_id INT, team_id INT)
--RETURNS TABLE (
-- reg_win INT,
-- reg_loss INT,
-- ot_win INT,
-- ot_loss INT,
-- tie INT,
-- game INT,
-- team INT
--) AS $$
--DECLARE
-- opponent_team_id INTEGER;
--BEGIN
-- IF NOT EXISTS (SELECT * FROM games WHERE games.id=game_id) THEN
-- RAISE EXCEPTION 'The game does not exist.';
-- END IF;
-- IF NOT EXISTS (SELECT * FROM teams WHERE teams.id=team_id) THEN
-- RAISE EXCEPTION 'The team does not exist.';
-- END IF;
-- IF NOT EXISTS (SELECT * FROM games JOIN teams ON teams.id=games.team_home OR teams.id=team_away WHERE games.id=game_id) THEN
-- RAISE EXCEPTION 'The team specified did not play this game.';
-- END IF;
--
-- SELECT
-- teams.id
-- INTO
-- opponent_team_id
-- FROM games
-- JOIN teams
-- ON (teams.id=games.team_home
-- OR teams.id=games.team_away)
-- WHERE games.id=game_id
-- AND teams.id!=team_id;
--
-- RETURN QUERY
-- SELECT
-- (CASE WHEN goals(game_id, team_id) > goals(game_id, opponent_team_id) AND periods(game_id) <= 3 THEN 1 ELSE 0 END) AS reg_win,
-- (CASE WHEN goals(game_id, team_id) < goals(game_id, opponent_team_id) AND periods(game_id) <= 3 THEN 1 ELSE 0 END) AS reg_loss,
-- (CASE WHEN goals(game_id, team_id) > goals(game_id, opponent_team_id) AND periods(game_id) > 3 THEN 1 ELSE 0 END) AS ot_win,
-- (CASE WHEN goals(game_id, team_id) < goals(game_id, opponent_team_id) AND periods(game_id) > 3 THEN 1 ELSE 0 END) AS ot_loss,
-- (CASE WHEN goals(game_id, team_id) = goals(game_id, opponent_team_id) THEN 1 ELSE 0 END) AS tie,
-- game_id AS game,
-- team_id AS team;
--END;
--$$ LANGUAGE plpgsql;
--
--CREATE OR REPLACE FUNCTION calculate_iihf_points(game_id INT, team_id INT)
--RETURNS INTEGER AS $$
--BEGIN
-- RETURN (
-- SELECT
-- (iihs_stats.reg_win * 3) +
-- (iihs_stats.reg_loss * 0) +
-- (iihs_stats.ot_win * 2) +
-- (iihs_stats.ot_loss * 1) +
-- (iihs_stats.tie * 2) AS points
-- FROM calculate_iihs_stats_stats(game_id, team_id) iihs_stats);
--END;
--$$ LANGUAGE plpgsql;

@ -0,0 +1 @@
DROP FUNCTION reg_win(INTEGER, INTEGER);

@ -0,0 +1,19 @@
CREATE FUNCTION reg_win(game_id INT, team_id INT)
RETURNS INTEGER
AS $$
DECLARE
opponent_team_id INTEGER;
BEGIN
SELECT
teams.id
INTO
opponent_team_id
FROM games
JOIN teams
ON (teams.id=games.team_home
OR teams.id=games.team_away)
WHERE games.id=game_id
AND teams.id!=team_id;
RETURN (SELECT (CASE WHEN goals(game_id, team_id) > goals(game_id, opponent_team_id) AND periods(game_id) <= 3 THEN 1 ELSE 0 END));
END;
$$ LANGUAGE plpgsql;

@ -0,0 +1 @@
DROP FUNCTION reg_loss(INTEGER, INTEGER);

@ -0,0 +1,19 @@
CREATE FUNCTION reg_loss(game_id INT, team_id INT)
RETURNS INTEGER
AS $$
DECLARE
opponent_team_id INTEGER;
BEGIN
SELECT
teams.id
INTO
opponent_team_id
FROM games
JOIN teams
ON (teams.id=games.team_home
OR teams.id=games.team_away)
WHERE games.id=game_id
AND teams.id!=team_id;
RETURN (SELECT (CASE WHEN goals(game_id, team_id) < goals(game_id, opponent_team_id) AND periods(game_id) <= 3 THEN 1 ELSE 0 END));
END;
$$ LANGUAGE plpgsql;

@ -0,0 +1 @@
DROP FUNCTION ot_win(INTEGER, INTEGER);

@ -0,0 +1,19 @@
CREATE FUNCTION ot_win(game_id INT, team_id INT)
RETURNS INTEGER
AS $$
DECLARE
opponent_team_id INTEGER;
BEGIN
SELECT
teams.id
INTO
opponent_team_id
FROM games
JOIN teams
ON (teams.id=games.team_home
OR teams.id=games.team_away)
WHERE games.id=game_id
AND teams.id!=team_id;
RETURN (SELECT (CASE WHEN goals(game_id, team_id) < goals(game_id, opponent_team_id) AND periods(game_id) > 3 THEN 1 ELSE 0 END));
END;
$$ LANGUAGE plpgsql;

@ -0,0 +1 @@
DROP FUNCTION ot_loss(INTEGER, INTEGER);

@ -0,0 +1,19 @@
CREATE FUNCTION ot_loss(game_id INT, team_id INT)
RETURNS INTEGER
AS $$
DECLARE
opponent_team_id INTEGER;
BEGIN
SELECT
teams.id
INTO
opponent_team_id
FROM games
JOIN teams
ON (teams.id=games.team_home
OR teams.id=games.team_away)
WHERE games.id=game_id
AND teams.id!=team_id;
RETURN (SELECT (CASE WHEN goals(game_id, team_id) < goals(game_id, opponent_team_id) AND periods(game_id) > 3 THEN 1 ELSE 0 END));
END;
$$ LANGUAGE plpgsql;

@ -0,0 +1 @@
DROP FUNCTION tie(INTEGER, INTEGER);

@ -0,0 +1,19 @@
CREATE FUNCTION tie(game_id INT, team_id INT)
RETURNS INTEGER
AS $$
DECLARE
opponent_team_id INTEGER;
BEGIN
SELECT
teams.id
INTO
opponent_team_id
FROM games
JOIN teams
ON (teams.id=games.team_home
OR teams.id=games.team_away)
WHERE games.id=game_id
AND teams.id!=team_id;
RETURN (SELECT (CASE WHEN goals(game_id, team_id) = goals(game_id, opponent_team_id) THEN 1 ELSE 0 END));
END;
$$ LANGUAGE plpgsql;

@ -22,24 +22,13 @@ BEGIN
RAISE EXCEPTION 'The team specified did not play this game.';
END IF;
SELECT
teams.id
INTO
opponent_team_id
FROM games
JOIN teams
ON (teams.id=games.team_home
OR teams.id=games.team_away)
WHERE games.id=game_id
AND teams.id!=team_id;
RETURN QUERY
SELECT
(CASE WHEN goals(game_id, team_id) > goals(game_id, opponent_team_id) AND periods(game_id) <= 3 THEN 1 ELSE 0 END) AS reg_win,
(CASE WHEN goals(game_id, team_id) < goals(game_id, opponent_team_id) AND periods(game_id) <= 3 THEN 1 ELSE 0 END) AS reg_loss,
(CASE WHEN goals(game_id, team_id) > goals(game_id, opponent_team_id) AND periods(game_id) > 3 THEN 1 ELSE 0 END) AS ot_win,
(CASE WHEN goals(game_id, team_id) < goals(game_id, opponent_team_id) AND periods(game_id) > 3 THEN 1 ELSE 0 END) AS ot_loss,
(CASE WHEN goals(game_id, team_id) = goals(game_id, opponent_team_id) THEN 1 ELSE 0 END) AS tie,
reg_win(game_id, team_id) AS reg_win,
reg_loss(game_id, team_id) AS reg_loss,
ot_win(game_id, team_id) AS ot_win,
ot_loss(game_id, team_id) AS ot_loss,
tie(game_id, team_id) AS tie,
game_id AS game,
team_id AS team;
END;

@ -4,11 +4,11 @@ RETURNS INTEGER AS $$
BEGIN
RETURN (
SELECT
(iihs_stats.reg_win * 3) +
(iihs_stats.reg_loss * 0) +
(iihs_stats.ot_win * 2) +
(iihs_stats.ot_loss * 1) +
(iihs_stats.tie * 2) AS points
FROM calculate_iihs_stats_stats(game_id, team_id) iihs_stats);
(iihf_stats.reg_win * 3) +
(iihf_stats.reg_loss * 0) +
(iihf_stats.ot_win * 2) +
(iihf_stats.ot_loss * 1) +
(iihf_stats.tie * 2) AS points
FROM iihf_stats(game_id, team_id) iihf_stats);
END;
$$ LANGUAGE plpgsql;

@ -1,51 +1,26 @@
mod db;
mod model;
mod views;
mod filters;
mod model;
mod translations;
mod views;
use translations::{
SupportedLanguage,
};
use crate::model::{
League,
Team,
Division,
GamePlayer,
Player,
Shot,
Game,
};
use views::{
GoalDetails,
PlayerStats,
TeamStats,
ShotDetails,
get_play_by_play_from_game,
};
use crate::model::{Division, Game, GamePlayer, League, Player, Shot, Team};
use translations::SupportedLanguage;
use views::{GoalDetails, PlayerStats, ShotDetails, TeamStats, IihfStatsI64};
use sqlx::{
Postgres,
Pool,
};
use ormx::Table;
use askama::Template;
use axum::{
Router,
extract::{Path, State},
http::StatusCode,
extract::{
Path,
State,
},
response::{
Json,
IntoResponse,
},
response::{IntoResponse, Json},
routing::get,
Router,
};
use axum_macros::debug_handler;
use ormx::Table;
use sqlx::{Pool, Postgres};
use std::net::SocketAddr;
use std::sync::Arc;
use askama::Template;
#[derive(Template)]
#[template(path = "hello.html")]
@ -91,10 +66,17 @@ struct LeagueListTemplate {
lang: SupportedLanguage,
}
#[derive(Template)]
#[template(path="partials/iihf_team_stats_table.html")]
struct IihfTeamStatsTableTemplate {
iihf_stats: Vec<IihfStatsI64>,
}
#[derive(Template)]
#[template(path = "game_list.html")]
struct GameListTemplate {
division: Division,
iihf_team_stats_table: IihfTeamStatsTableTemplate,
games: Vec<Game>,
lang: SupportedLanguage,
}
@ -158,7 +140,10 @@ async fn main() {
.unwrap();
}
async fn player_from_name(State(server_config): State<ServerState>, Path((lang,name)): Path<(SupportedLanguage,String)>) -> impl IntoResponse {
async fn player_from_name(
State(server_config): State<ServerState>,
Path((lang, name)): Path<(SupportedLanguage, String)>,
) -> impl IntoResponse {
let player = Player::from_name_case_insensitive(&server_config.db_pool, name)
.await
.unwrap();
@ -166,7 +151,8 @@ async fn player_from_name(State(server_config): State<ServerState>, Path((lang,n
.await
.unwrap()
.unwrap();
let latest_league_stats = League::player_stats(&server_config.db_pool, player.id, latest_league.id)
let latest_league_stats =
League::player_stats(&server_config.db_pool, player.id, latest_league.id)
.await
.unwrap();
let lifetime_stats = Player::lifetime_stats(&server_config.db_pool, player.id)
@ -183,40 +169,46 @@ async fn player_from_name(State(server_config): State<ServerState>, Path((lang,n
}
async fn test_template<'a>() -> HelloTemplate<'a> {
HelloTemplate { name: "Tait", years: 24 }
HelloTemplate {
name: "Tait",
years: 24,
}
}
macro_rules! get_all {
($crud_struct:ident, $func_name:ident) => {
#[debug_handler]
async fn $func_name(State(server_config): State<ServerState>) -> impl IntoResponse {
let cruder = $crud_struct::all(&*server_config.db_pool)
.await
.unwrap();
let cruder = $crud_struct::all(&*server_config.db_pool).await.unwrap();
(StatusCode::OK, Json(cruder))
}
}
};
}
macro_rules! get_by_id {
($crud_struct:ident, $func_name:ident) => {
#[debug_handler]
async fn $func_name(State(server_config): State<ServerState>, Path(id): Path<i32>) -> impl IntoResponse {
async fn $func_name(
State(server_config): State<ServerState>,
Path(id): Path<i32>,
) -> impl IntoResponse {
let cruder = $crud_struct::get(&*server_config.db_pool, id)
.await
.unwrap();
(StatusCode::OK, Json(cruder))
}
}
};
}
async fn league_html(State(server_config): State<ServerState>, Path(lang): Path<SupportedLanguage>) -> impl IntoResponse {
let leagues = League::all(&*server_config.db_pool)
.await
.unwrap();
async fn league_html(
State(server_config): State<ServerState>,
Path(lang): Path<SupportedLanguage>,
) -> impl IntoResponse {
let leagues = League::all(&*server_config.db_pool).await.unwrap();
let heading = match lang {
SupportedLanguage::English => "IBIHF Leagues",
SupportedLanguage::French => "League de FIDHS",
}.to_string();
}
.to_string();
let leagues_template = LeagueListTemplate {
leagues,
heading,
@ -225,7 +217,10 @@ async fn league_html(State(server_config): State<ServerState>, Path(lang): Path<
(StatusCode::OK, leagues_template)
}
async fn divisions_for_league_html(State(server_config): State<ServerState>, Path((lang,league_id)): Path<(SupportedLanguage, i32)>) -> impl IntoResponse {
async fn divisions_for_league_html(
State(server_config): State<ServerState>,
Path((lang, league_id)): Path<(SupportedLanguage, i32)>,
) -> impl IntoResponse {
let league = League::get(&*server_config.db_pool, league_id)
.await
.unwrap();
@ -240,24 +235,34 @@ async fn divisions_for_league_html(State(server_config): State<ServerState>, Pat
(StatusCode::OK, html)
}
async fn games_for_division_html(State(server_config): State<ServerState>, Path((lang,division_id)): Path<(SupportedLanguage,i32)>) -> impl IntoResponse {
async fn games_for_division_html(
State(server_config): State<ServerState>,
Path((lang, division_id)): Path<(SupportedLanguage, i32)>,
) -> impl IntoResponse {
let division = Division::get(&*server_config.db_pool, division_id)
.await
.unwrap();
let games = Game::by_division(&*server_config.db_pool, division.id)
.await
.unwrap();
let iihf_stats = division.iihf_stats(&*server_config.db_pool)
.await
.unwrap();
let games_template = GameListTemplate {
division,
iihf_team_stats_table: IihfTeamStatsTableTemplate {
iihf_stats,
},
games,
lang,
};
(StatusCode::OK, games_template)
}
async fn score_for_game_html(State(server_config): State<ServerState>, Path((lang,game_id)): Path<(SupportedLanguage, i32)>) -> impl IntoResponse {
let game = sqlx::query_as::<_, Game>(
"SELECT * FROM games WHERE id = $1;"
)
async fn score_for_game_html(
State(server_config): State<ServerState>,
Path((lang, game_id)): Path<(SupportedLanguage, 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
@ -265,17 +270,24 @@ async fn score_for_game_html(State(server_config): State<ServerState>, Path((lan
let division = Division::get(&*server_config.db_pool, game.division)
.await
.unwrap();
let pbp = get_play_by_play_from_game(&server_config.db_pool, &game).await.unwrap();
let score = Game::score(&server_config.db_pool, game.id).await.unwrap();
let pbp = game.play_by_play(&server_config.db_pool)
.await
.unwrap();
let score = game.score(&server_config.db_pool).await.unwrap();
let score_html = TeamGameStatsTemplate { teams: score, lang };
let goal_details = Game::box_score(&server_config.db_pool, game.id).await.unwrap();
let goal_details_html = IndividualGamePointsTableTemplate { players: goal_details, lang };
let box_score = Game::goals(&server_config.db_pool, game.id).await.unwrap();
let box_score_html = BoxScoreTemplate { goals: box_score, lang };
let pbp_html = ShotsTableTemplate {
shots: pbp,
let goal_details = game.box_score(&server_config.db_pool)
.await
.unwrap();
let goal_details_html = IndividualGamePointsTableTemplate {
players: goal_details,
lang,
};
let box_score = game.goals(&server_config.db_pool).await.unwrap();
let box_score_html = BoxScoreTemplate {
goals: box_score,
lang,
};
let pbp_html = ShotsTableTemplate { shots: pbp, lang };
let game_template = GameScorePageTemplate {
division,
game,
@ -306,36 +318,12 @@ macro_rules! impl_all_query_types {
($ty:ident, $func_all:ident, $func_by_id:ident) => {
get_all!($ty, $func_all);
get_by_id!($ty, $func_by_id);
}
};
}
impl_all_query_types!(
GamePlayer,
game_player_all,
game_player_id
);
impl_all_query_types!(
Player,
player_all,
player_id
);
impl_all_query_types!(
Team,
team_all,
team_id
);
impl_all_query_types!(
Shot,
shots_all,
shots_id
);
impl_all_query_types!(
Division,
division_all,
division_id
);
impl_all_query_types!(
League,
league_all,
league_id
);
impl_all_query_types!(GamePlayer, game_player_all, game_player_id);
impl_all_query_types!(Player, player_all, player_id);
impl_all_query_types!(Team, team_all, team_id);
impl_all_query_types!(Shot, shots_all, shots_id);
impl_all_query_types!(Division, division_all, division_id);
impl_all_query_types!(League, league_all, league_id);

@ -1,13 +1,7 @@
use crate::model::{Division, Game, League, Period, Player};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use sqlx::PgPool;
use crate::model::{
Player,
Game,
League,
Division,
Period,
};
use serde::{Serialize, Deserialize};
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct TeamStats {
@ -17,7 +11,29 @@ pub struct TeamStats {
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct IihfGameStats {
pub struct IihfStats {
pub team_name: String,
pub team_id: i32,
pub reg_wins: i32,
pub reg_losses: i32,
pub ot_wins: i32,
pub ot_losses: i32,
pub ties: i32,
pub points: i32,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct IihfStatsI64 {
pub team_name: String,
pub team_id: i32,
pub reg_wins: i64,
pub reg_losses: i64,
pub ot_wins: i64,
pub ot_losses: i64,
pub ties: i64,
pub points: i64,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct IihfPoints {
pub team_name: String,
pub team_id: i32,
pub points: i32,
@ -41,8 +57,7 @@ pub struct PlayerStats {
pub points: i64,
}
impl Game {
pub async fn box_score(pool: &PgPool, id: i32) -> Result<Vec<PlayerStats>, sqlx::Error> {
pub async fn game_box_score(pool: &PgPool, game_id: i32) -> Result<Vec<PlayerStats>, sqlx::Error> {
let query = r#"
SELECT
COUNT(shots.id) AS points,
@ -50,7 +65,8 @@ impl Game {
COUNT(CASE WHEN shots.assistant = game_players.id OR shots.assistant_second = game_players.id THEN shots.id END) AS assists,
players.name
FROM game_players
JOIN players ON game_players.player = players.id
JOIN players
ON game_players.player = players.id
LEFT JOIN shots
ON shots.goal=true
AND (shots.shooter=game_players.id
@ -66,11 +82,11 @@ impl Game {
goals DESC;
"#;
sqlx::query_as::<_, PlayerStats>(query)
.bind(id)
.bind(game_id)
.fetch_all(pool)
.await
}
pub async fn goals(pool: &PgPool, id: i32) -> Result<Vec<GoalDetails>, sqlx::Error> {
pub async fn game_goals(pool: &PgPool, game_id: i32) -> Result<Vec<GoalDetails>, sqlx::Error> {
sqlx::query_as::<_, GoalDetails>(
r#"
SELECT
@ -104,78 +120,23 @@ impl Game {
ORDER BY
periods.period_type ASC,
shots.period_time DESC;
"#)
.bind(id)
"#,
)
.bind(game_id)
.fetch_all(pool)
.await
}
pub async fn iihf_stats(pool: &PgPool, game_id: i32) -> Result<Vec<IihfGameStats>, sqlx::Error> {
pub async fn game_iihf_stats(pool: &PgPool, game_id: i32) -> Result<Vec<IihfStats>, sqlx::Error> {
let query = r#"
SELECT
(CASE WHEN
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id=teams.id
THEN shots.id
END) >
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id!=teams.id
THEN shots.id
END)
AND (SELECT COUNT(id) FROM periods WHERE periods.game=games.id) <= 3
THEN 1
ELSE 0
END) AS reg_wins,
(CASE WHEN
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id=teams.id
THEN shots.id
END) <
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id!=teams.id
THEN shots.id
END)
AND (SELECT COUNT(id) FROM periods WHERE periods.game=games.id) <= 3
THEN 1
ELSE 0
END) AS reg_losses,
(CASE WHEN
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id=teams.id
THEN shots.id
END) >
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id!=teams.id
THEN shots.id
END)
AND (SELECT COUNT(id) FROM periods WHERE periods.game=games.id) > 3
THEN 1
ELSE 0
END) AS ot_wins,
(CASE WHEN
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id=teams.id
THEN shots.id
END) <
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id!=teams.id
THEN shots.id
END)
AND (SELECT COUNT(id) FROM periods WHERE periods.game=games.id) > 3
THEN 1
ELSE 0
END) AS ot_losses,
(CASE WHEN
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id=teams.id
THEN shots.id
END) =
COUNT(CASE WHEN shots.goal=true
AND scoring_team.id!=teams.id
THEN shots.id
END)
THEN 1
ELSE 0
END) AS ties
teams.id AS team_id,
teams.name AS team_name,
reg_win(games.id, teams.id) AS reg_wins,
reg_loss(games.id, teams.id) AS reg_losses,
ot_win(games.id, teams.id) AS ot_wins,
ot_loss(games.id, teams.id) AS ot_losses,
tie(games.id, teams.id) AS ties,
iihf_points(games.id, teams.id) AS points
FROM games
JOIN periods ON periods.game=games.id
JOIN shots ON shots.period=periods.id
@ -188,7 +149,7 @@ impl Game {
WHERE games.id=4
GROUP BY teams.id,games.id;
"#;
sqlx::query_as::<_, IihfGameStats>(query)
sqlx::query_as::<_, IihfStats>(query)
.bind(game_id)
.fetch_all(pool)
.await
@ -197,10 +158,10 @@ impl Game {
/// NOTE: The algorithm used here requires that a 4th period is the "overtime";
/// it does not check if there was only two periods, followed by an overtime.
/// This should be sufficient for most.
pub async fn iihf_score(pool: &PgPool, game_id: i32) -> Result<Vec<IihfGameStats>, sqlx::Error> {
pub async fn game_iihf_points(pool: &PgPool, game_id: i32) -> Result<Vec<IihfPoints>, sqlx::Error> {
let query = r#"
SELECT
calculate_iihf_points(games.id, teams.id) AS points,
iihf_points(games.id, teams.id) AS points,
teams.name AS team_name,
teams.id AS team_id
FROM games
@ -210,13 +171,13 @@ impl Game {
WHERE games.id=$1
ORDER BY points;
"#;
sqlx::query_as::<_, IihfGameStats>(query)
sqlx::query_as::<_, IihfPoints>(query)
.bind(game_id)
.fetch_all(pool)
.await
}
/// Returns the number of shots and goals for each team in the game.
pub async fn score(pool: &PgPool, game_id: i32) -> Result<Vec<TeamStats>, sqlx::Error> {
pub async fn game_score(pool: &PgPool, game_id: i32) -> Result<Vec<TeamStats>, sqlx::Error> {
let query = r#"
SELECT
COUNT(CASE WHEN shots.goal = true THEN shots.id END) AS goals,
@ -235,12 +196,106 @@ impl Game {
.fetch_all(pool)
.await
}
pub async fn game_play_by_play(
pool: &PgPool,
game_id: i32,
) -> Result<Vec<ShotDetails>, sqlx::Error> {
sqlx::query_as::<_, ShotDetails>(
r#"
SELECT
shots.shooter AS player_id,
shots.assistant AS first_assist_id,
shots.assistant_second AS second_assist_id,
shots.goal AS is_goal,
players.name AS player_name,
p_assistant.name AS first_assist_name,
p_assistant_second.name AS second_assist_name,
game_players.player_number AS player_number,
gp_assistant.player_number AS first_assist_number,
gp_assistant_second.player_number AS second_assist_number,
teams.name AS team_name,
teams.id AS team_id,
shots.period_time AS time_remaining,
period_types.id AS period_id,
period_types.short_name AS period_short_name
FROM shots
JOIN game_players ON game_players.id=shots.shooter
JOIN players ON players.id=game_players.player
JOIN teams ON teams.id=game_players.team
LEFT JOIN game_players gp_assistant ON gp_assistant.id=shots.assistant
LEFT JOIN players p_assistant ON p_assistant.id=gp_assistant.player
LEFT JOIN game_players gp_assistant_second ON gp_assistant_second.id=shots.assistant_second
LEFT JOIN players p_assistant_second ON p_assistant_second.id=gp_assistant_second.player
JOIN periods ON shots.period=periods.id
JOIN period_types ON periods.period_type=period_types.id
WHERE periods.game=$1
ORDER BY
periods.period_type ASC,
shots.period_time DESC;
"#,
)
.bind(game_id)
.fetch_all(pool)
.await
}
impl Game {
pub async fn score(&self, pool: &PgPool) -> Result<Vec<TeamStats>, sqlx::Error> {
game_score(pool, self.id).await
}
pub async fn box_score(&self, pool: &PgPool) -> Result<Vec<PlayerStats>, sqlx::Error> {
game_box_score(pool, self.id).await
}
pub async fn iihf_points(&self, pool: &PgPool) -> Result<Vec<IihfPoints>, sqlx::Error> {
game_iihf_points(pool, self.id).await
}
pub async fn iihf_stats(&self, pool: &PgPool) -> Result<Vec<IihfStats>, sqlx::Error> {
game_iihf_stats(pool, self.id).await
}
pub async fn goals(&self, pool: &PgPool) -> Result<Vec<GoalDetails>, sqlx::Error> {
game_goals(pool, self.id).await
}
pub async fn play_by_play(&self, pool: &PgPool) -> Result<Vec<ShotDetails>, sqlx::Error> {
game_play_by_play(pool, self.id).await
}
}
pub async fn division_iihf_stats(pool: &PgPool, division_id: i32) -> Result<Vec<IihfStatsI64>, sqlx::Error> {
sqlx::query_as::<_, IihfStatsI64>(
r#"
SELECT
SUM(reg_win(games.id, teams.id)) AS reg_wins,
SUM(reg_loss(games.id, teams.id)) AS reg_losses,
SUM(ot_win(games.id, teams.id)) AS ot_wins,
SUM(ot_loss(games.id, teams.id)) AS ot_losses,
SUM(tie(games.id, teams.id)) AS ties,
SUM(iihf_points(games.id, teams.id)) AS points,
teams.id AS team_id,
teams.name AS team_name
FROM
games
JOIN teams ON teams.id=games.team_home OR teams.id=games.team_away
WHERE games.division=$1
GROUP BY
teams.id
ORDER BY
points DESC;
"#
)
.bind(division_id)
.fetch_all(pool)
.await
}
impl Division {
pub async fn iihf_stats(&self, pool: &PgPool) -> Result<Vec<IihfStatsI64>, sqlx::Error> {
division_iihf_stats(pool, self.id).await
}
}
impl Player {
pub async fn latest_league(pool: &PgPool, id: i32) -> Result<Option<League>, sqlx::Error> {
let query =
r#"
let query = r#"
SELECT leagues.*
FROM players
JOIN game_players ON game_players.player=players.id
@ -258,8 +313,7 @@ impl Player {
.await
}
pub async fn latest_stats(pool: &PgPool, id: i32) -> Result<Vec<GoalDetails>, sqlx::Error> {
let query =
r#"
let query = r#"
SELECT
players.id AS player_id,
p_assist.id AS first_assist_id,
@ -354,7 +408,11 @@ ORDER BY
}
impl League {
pub async fn player_stats(pool: &PgPool, player_id: i32, league_id: i32) -> Result<PlayerStats, sqlx::Error> {
pub async fn player_stats(
pool: &PgPool,
player_id: i32,
league_id: i32,
) -> Result<PlayerStats, sqlx::Error> {
let query = r#"
SELECT
COUNT(goals.id) AS goals,
@ -390,8 +448,6 @@ impl League {
}
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct GoalDetails {
pub player_id: i32,
@ -428,73 +484,18 @@ pub struct ShotDetails {
pub second_assist_number: Option<i32>,
}
pub async fn get_play_by_play_from_game(pool: &PgPool, game: &Game) -> Result<Vec<ShotDetails>, sqlx::Error> {
sqlx::query_as::<_, ShotDetails>(
r#"
SELECT
shots.shooter AS player_id,
shots.assistant AS first_assist_id,
shots.assistant_second AS second_assist_id,
shots.goal AS is_goal,
players.name AS player_name,
p_assistant.name AS first_assist_name,
p_assistant_second.name AS second_assist_name,
game_players.player_number AS player_number,
gp_assistant.player_number AS first_assist_number,
gp_assistant_second.player_number AS second_assist_number,
teams.name AS team_name,
teams.id AS team_id,
shots.period_time AS time_remaining,
period_types.id AS period_id,
period_types.short_name AS period_short_name
FROM shots
JOIN game_players ON game_players.id=shots.shooter
JOIN players ON players.id=game_players.player
JOIN teams ON teams.id=game_players.team
LEFT JOIN game_players gp_assistant ON gp_assistant.id=shots.assistant
LEFT JOIN players p_assistant ON p_assistant.id=gp_assistant.player
LEFT JOIN game_players gp_assistant_second ON gp_assistant_second.id=shots.assistant_second
LEFT JOIN players p_assistant_second ON p_assistant_second.id=gp_assistant_second.player
JOIN periods ON shots.period=periods.id
JOIN period_types ON periods.period_type=period_types.id
WHERE periods.game=$1
ORDER BY
periods.period_type ASC,
shots.period_time DESC;
"#)
.bind(game.id)
.fetch_all(pool)
.await
}
#[cfg(test)]
mod tests {
use std::env;
use crate::model::{Game, League, Player};
use crate::views::{game_play_by_play, get_player_stats_overview, Notification, game_score, game_goals, game_iihf_stats, game_iihf_points, game_box_score, division_iihf_stats};
use ormx::Table;
use crate::model::{
Game,
Player,
League,
};
use crate::views::{
Notification,
get_player_stats_overview,
get_play_by_play_from_game,
};
use std::env;
#[test]
fn check_play_by_play() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let game = Game::get(&pool, 3)
.await
.unwrap();
let pbp = get_play_by_play_from_game(&pool, &game)
.await
.unwrap();
let pbp = game_play_by_play(&pool, 3).await.unwrap();
})
}
@ -502,12 +503,8 @@ mod tests {
fn get_latest_stats_of_player() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let player = Player::get(&pool, 2)
.await
.unwrap();
let latest = Player::latest_stats(&pool, player.id)
.await
.unwrap();
let player = Player::get(&pool, 2).await.unwrap();
let latest = Player::latest_stats(&pool, player.id).await.unwrap();
})
}
@ -515,13 +512,11 @@ mod tests {
fn check_league_player_stats() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let league = League::get(&pool, 1)
.await
.unwrap();
let player = Player::get(&pool, 2)
let league = League::get(&pool, 1).await.unwrap();
let player = Player::get(&pool, 2).await.unwrap();
let stats = League::player_stats(&pool, player.id, league.id)
.await
.unwrap();
let stats = League::player_stats(&pool, player.id, league.id).await.unwrap();
assert_eq!(stats.name, "Hillary Scanlon");
})
}
@ -530,9 +525,7 @@ mod tests {
fn check_latest_league_for_player() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let player = Player::get(&pool, 5)
.await
.unwrap();
let player = Player::get(&pool, 5).await.unwrap();
let league = Player::latest_league(&pool, player.id)
.await
.unwrap()
@ -545,12 +538,7 @@ mod tests {
fn check_score_details_from_game() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let game = Game::get(&pool, 3)
.await
.unwrap();
let scores = Game::goals(&pool, game.id)
.await
.unwrap();
let scores = game_goals(&pool, 3).await.unwrap();
println!("{scores:?}");
})
}
@ -559,32 +547,55 @@ mod tests {
fn check_box_score_from_game() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let game = Game::get(&pool, 4)
.await
.unwrap();
let scores = Game::box_score(&pool, game.id)
.await
.unwrap();
let scores = game_box_score(&pool, 4).await.unwrap();
println!("{scores:?}");
let second_top_scorer = scores.get(1).unwrap();
assert_eq!(second_top_scorer.name, "Allyssa Foulds");
assert_eq!(second_top_scorer.goals, 1, "Allysa should have 1 goal..");
assert_eq!(second_top_scorer.assists, 2, "Allyssa should have 2 assists.");
assert_eq!(
second_top_scorer.assists, 2,
"Allyssa should have 2 assists."
);
assert_eq!(second_top_scorer.points, 3, "Allysa should have 3 points.");
assert_eq!(scores.len(), 8, "Players which did not receive any points should not be in the box score.");
assert_eq!(
scores.len(),
8,
"Players which did not receive any points should not be in the box score."
);
})
}
#[test]
fn check_iihf_score() {
fn check_division_iihf_points() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let game = Game::get(&pool, 4)
.await
.unwrap();
let score = Game::iihf_score(&pool, game.id)
.await
.unwrap();
let score = division_iihf_stats(&pool, 1).await.unwrap();
assert_eq!(score.get(0).unwrap().points, 10);
assert_eq!(score.get(0).unwrap().team_name, "Bullseye");
assert_eq!(score.get(0).unwrap().reg_losses, 0);
assert_eq!(score.get(0).unwrap().ties, 2);
assert_eq!(score.get(1).unwrap().points, 4);
})
}
#[test]
fn check_iihf_stats() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let score = game_iihf_stats(&pool, 4).await.unwrap();
assert_eq!(score.get(0).unwrap().points, 2);
assert_eq!(score.get(0).unwrap().team_name, "Bullseye");
assert_eq!(score.get(0).unwrap().reg_losses, 0);
assert_eq!(score.get(0).unwrap().ties, 1);
assert_eq!(score.get(1).unwrap().points, 2);
})
}
#[test]
fn check_iihf_points() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let score = game_iihf_points(&pool, 4).await.unwrap();
assert_eq!(score.get(0).unwrap().points, 2);
assert_eq!(score.get(0).unwrap().team_name, "Bullseye");
assert_eq!(score.get(1).unwrap().points, 2);
@ -595,12 +606,7 @@ mod tests {
fn check_game_score() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let game = Game::get(&pool, 1)
.await
.unwrap();
let score = Game::score(&pool, game.id)
.await
.unwrap();
let score = game_score(&pool, 1).await.unwrap();
assert_eq!(score.get(0).unwrap().goals, 1);
assert_eq!(score.get(1).unwrap().goals, 1);
})
@ -621,9 +627,7 @@ mod tests {
fn check_lifetime_stats() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let lifetime_stats = Player::lifetime_stats(&pool, 5)
.await
.unwrap();
let lifetime_stats = Player::lifetime_stats(&pool, 5).await.unwrap();
})
}
@ -654,7 +658,8 @@ JOIN positions ON positions.id=game_players.position;
.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}",
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,
@ -668,12 +673,12 @@ JOIN positions ON positions.id=game_players.position;
/// 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.");
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")
}
}

@ -1,10 +1,13 @@
{% extends "master.html" %}
{% block title %}Games{% endblock %}
{% block title %}Games for {{ division.name }}{% endblock %}
{% block content %}
<h1 id="games">Games for {{ division.name }}</h1>
<h1>Division: {{ division.name }}</h1>
{% if games.len() > 0 %}
<h2 id="iihf_points">Points</h2>
{{ iihf_team_stats_table|safe }}
<h2 id="games">Games</h2>
<ol aria-labelledby="games">
{% for game in games %}
<li><a href="/en/game/{{ game.id }}/">{{ game.name }}</a></li>

@ -0,0 +1,26 @@
<table>
<thead>
<tr>
<th>Team</th>
<th>Points</th>
<th>RW</th>
<th>RL</th>
<th>OW</th>
<th>OL</th>
<th>T</th>
</tr>
</thead>
<tbody>
{% for team in iihf_stats %}
<tr>
<td>{{ team.team_name }}</td>
<td>{{ team.points }}</td>
<td>{{ team.reg_wins }}</td>
<td>{{ team.reg_losses }}</td>
<td>{{ team.ot_wins }}</td>
<td>{{ team.ot_losses }}</td>
<td>{{ team.ties }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Loading…
Cancel
Save