master
Tait Hoyem 1 year ago
parent 0d2e4f3574
commit b0b0e8c33d

@ -1,8 +1,9 @@
-- Add up migration script here
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),
-- this allows there to be special names like "Gold Medal Game", but the default will be the number of games already in the division + 1
-- NOTE: this is only done in the front end, the backend will not give a default value
name VARCHAR(255) NOT NULL,
-- what divison is the game a part of (usefl for stats)
division INTEGER NOT NULL,
team_home INTEGER NOT NULL,

@ -0,0 +1 @@
DROP TABLE IF EXISTS period_types;

@ -1,8 +1,10 @@
CREATE TABLE IF NOT EXISTS periods (
CREATE TABLE IF NOT EXISTS period_types (
id SERIAL PRIMARY KEY NOT NULL,
-- "first", "second", "third", "second overtime", "shootout"
name VARCHAR(32) NOT NULL,
-- "1", "2", "3", "OT", "[2-9]OT", "SO"
-- technically 10+OT would not work, but this should be rare enough to not worry about.
short_name VARCHAR(3) NOT NULL
short_name VARCHAR(3) NOT NULL,
-- default length
default_length INTEGER NOT NULL
);

@ -0,0 +1,16 @@
INSERT INTO period_types
(id, name, short_name, default_length)
VALUES
(1, 'first', '1', 1200),
(2, 'second', '2', 1200),
(3, 'third', '3', 1200),
(4, 'overtime', 'OT', 300),
(5, 'shootout', 'SO', 0),
(6, 'second overtime', '2OT', 1200),
(7, 'third overtime', '3OT', 1200),
(8, 'fourth overtime', '4OT', 1200),
(9, 'fifth overtime', '5OT', 1200),
(10, 'sixth overtime', '6OT', 1200),
(11, 'seventh overtime', '7OT', 1200),
(12, 'eighth overtime', '8OT', 1200),
(13, 'ninth overtime', '9OT', 1200);

@ -1,16 +0,0 @@
INSERT INTO periods
(id, name, short_name)
VALUES
(1, 'first', '1'),
(2, 'second', '2'),
(3, 'third', '3'),
(4, 'overtime', 'OT'),
(5, 'shootout', 'SO'),
(6, 'second overtime', '2OT'),
(7, 'third overtime', '3OT'),
(8, 'fourth overtime', '4OT'),
(9, 'fifth overtime', '5OT'),
(10, 'sixth overtime', '6OT'),
(11, 'seventh overtime', '7OT'),
(12, 'eighth overtime', '8OT'),
(13, 'ninth overtime', '9OT');

@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS periods (
id SERIAL PRIMARY KEY NOT NULL,
-- which kind of period is it: 1st, 2nd, third, SO, OT, 5OT, etc.
period_type INTEGER NOT NULL,
-- length of period in seconds
period_length INTEGER NOT NULL,
-- which game does this period refer to
game INTEGER NOT NULL,
-- period type must exists
CONSTRAINT period_type_fk
FOREIGN KEY(period_type)
REFERENCES period_types(id)
ON DELETE RESTRICT,
-- game must exist
CONSTRAINT game_fk
FOREIGN KEY(game)
REFERENCES games(id)
ON DELETE RESTRICT
);

@ -16,8 +16,6 @@ CREATE TABLE IF NOT EXISTS shots (
shooter INTEGER NOT NULL,
-- which player was the goalie
goalie INTEGER NOT NULL,
-- which game was this a part of
game INTEGER NOT NULL,
-- which period did the shot happen in
period INTEGER NOT NULL,
-- when did the shot happen relative to the beginning of the period
@ -25,6 +23,8 @@ CREATE TABLE IF NOT EXISTS shots (
-- if applicable, set assistant(s)
assistant INTEGER,
assistant_second INTEGER,
-- when was the record created
created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,
-- was the shooter a real player
CONSTRAINT shooter_fk
FOREIGN KEY(shooter)
@ -55,12 +55,7 @@ CREATE TABLE IF NOT EXISTS shots (
FOREIGN KEY(shooter_team)
REFERENCES teams(id)
ON DELETE RESTRICT,
-- is the game a real game
CONSTRAINT game_fk
FOREIGN KEY(game)
REFERENCES games(id)
ON DELETE RESTRICT,
-- is the period refgerences a real period type
-- is the period references a real period
CONSTRAINT period_fk
FOREIGN KEY(period)
REFERENCES periods(id)

@ -0,0 +1,63 @@
INSERT INTO periods
(game, period_type, period_length)
VALUES
(
1,
1,
1200
),
(
1,
2,
900
),
(
1,
3,
900
),
(
2,
1,
1200
),
(
2,
2,
1200
),
(
2,
3,
1200
),
(
3,
1,
720
),
(
3,
2,
720
),
(
3,
3,
1200
),
(
4,
1,
1200
),
(
4,
2,
1200
),
(
4,
3,
1200
);

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
pub fn seconds_as_time(secs: &i32) -> ::askama::Result<String> {
let minutes = secs / 60;
let seconds = secs % 60;
Ok(format!("{}:{}", minutes, seconds))
}

@ -1,9 +1,9 @@
mod db;
mod model;
mod views;
mod filters;
use crate::model::{
TableName,
League,
Team,
Division,
@ -13,14 +13,24 @@ use crate::model::{
Game,
};
use views::{
GoalDetails,
PlayerStats,
TeamStats,
ShotDetails,
get_score_from_game,
get_box_score_from_game,
get_play_by_play_from_game,
get_goals_from_game,
get_latest_league_for_player,
get_league_player_stats,
get_all_player_stats,
};
use sqlx::{
Postgres,
Pool,
};
use ormx::Table;
use axum::{
Router,
http::StatusCode,
@ -38,6 +48,78 @@ use axum::{
use axum_macros::debug_handler;
use std::net::SocketAddr;
use std::sync::Arc;
use askama::Template;
#[derive(Template)]
#[template(path="hello.html")]
struct HelloTemplate<'a> {
name: &'a str,
years: i32
}
#[derive(Template)]
#[template(path="partials/box_score_table.html")]
struct BoxScoreTemplate {
goals: Vec<GoalDetails>,
}
#[derive(Template)]
#[template(path="partials/individual_game_points_table.html")]
struct IndividualGamePointsTableTemplate {
players: Vec<PlayerStats>,
}
#[derive(Template)]
#[template(path="partials/team_stats_table.html")]
struct TeamGameStatsTemplate {
teams: Vec<TeamStats>,
}
#[derive(Template)]
#[template(path="division_list.html")]
struct DivisionListTemplate {
league: League,
divisions: Vec<Division>,
}
#[derive(Template)]
#[template(path="league_list.html")]
struct LeagueListTemplate {
leagues: Vec<League>,
}
#[derive(Template)]
#[template(path="game_list.html")]
struct GameListTemplate {
division: Division,
games: Vec<Game>,
}
#[derive(Template)]
#[template(path="partials/play_by_play_table.html")]
struct ShotsTableTemplate {
shots: Vec<ShotDetails>,
}
#[derive(Template)]
#[template(path="game_score_page.html")]
struct GameScorePageTemplate {
game: Game,
division: Division,
box_score: BoxScoreTemplate,
team_stats: TeamGameStatsTemplate,
individual_stats: IndividualGamePointsTableTemplate,
play_by_play: ShotsTableTemplate,
}
#[derive(Template)]
#[template(path="player_page.html")]
pub struct PlayerPageTemplate {
player: Player,
league: League,
league_stats: PlayerStats,
lifetime_stats: PlayerStats,
}
#[derive(Clone)]
pub struct ServerState {
@ -52,9 +134,12 @@ async fn main() {
};
let router = Router::new()
.route("/", get(league_html))
.route("/league/:id/divisions/", get(divisions_for_league_html))
.route("/shots/", get(shots_all))
.route("/test/", get(test_template))
.route("/league/:id/", get(divisions_for_league_html))
.route("/division/:id/", get(games_for_division_html))
.route("/game/:id/", get(score_for_game_html))
.route("/player/:name/", get(player_from_name))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
println!("Listening on {}", addr);
@ -64,39 +149,38 @@ 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)
)
.fetch_all(pool)
.await
}
async fn get_by_id<T: Send + Unpin + TableName + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool, id: i32) -> Result<Option<T>, sqlx::Error> {
sqlx::query_as::<_, T>(
&format!("SELECT * FROM {} WHERE id = $1;", <T as TableName>::TABLE_NAME)
)
.bind(id)
.fetch_optional(pool)
.await
async fn player_from_name(State(server_config): State<ServerState>, Path(name): Path<String>) -> impl IntoResponse {
let player = Player::from_name_case_insensitive(&server_config.db_pool, name)
.await
.unwrap();
let latest_league = get_latest_league_for_player(&server_config.db_pool, &player)
.await
.unwrap()
.unwrap();
let latest_league_stats = get_league_player_stats(&server_config.db_pool, &player, &latest_league)
.await
.unwrap();
let lifetime_stats = get_all_player_stats(&server_config.db_pool, &player)
.await
.unwrap();
let html = PlayerPageTemplate {
player,
league: latest_league,
league_stats: latest_league_stats,
lifetime_stats,
};
(StatusCode::OK, html)
}
/*
async fn insert_into<T: Sync + Send + Unpin + TableName + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool, new: &T) -> Result<sqlx::postgres::PgQueryResult, sqlx::Error> {
let query = sql_builder::SqlBuilder::insert_into(<T as TableName>::TABLE_NAME)
.values(())
.sql().unwrap();
sqlx::query(
&query
)
.execute(pool)
.await
async fn test_template<'a>() -> HelloTemplate<'a> {
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 = get_all::<$crud_struct>(&server_config.db_pool)
let cruder = $crud_struct::all(&*server_config.db_pool)
.await
.unwrap();
(StatusCode::OK, Json(cruder))
@ -107,7 +191,7 @@ 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 {
let cruder = get_by_id::<$crud_struct>(&server_config.db_pool, id)
let cruder = $crud_struct::get(&*server_config.db_pool, id)
.await
.unwrap();
(StatusCode::OK, Json(cruder))
@ -116,60 +200,41 @@ 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))
let leagues = League::all(&*server_config.db_pool)
.await
.unwrap();
let leagues_template = LeagueListTemplate {
leagues
};
(StatusCode::OK, leagues_template)
}
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)
let league = League::get(&*server_config.db_pool, league_id)
.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))
.unwrap();
let divisions = Division::by_league(&*server_config.db_pool, league_id)
.await
.unwrap();
let html = DivisionListTemplate {
league,
divisions
};
(StatusCode::OK, 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)
let division = Division::get(&*server_config.db_pool, division_id)
.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))
.unwrap();
let games = Game::by_division(&*server_config.db_pool, division.id)
.await
.unwrap();
let games_template = GameListTemplate {
division,
games
};
(StatusCode::OK, games_template)
}
async fn score_for_game_html(State(server_config): State<ServerState>, Path(game_id): Path<i32>) -> impl IntoResponse {
let game = sqlx::query_as::<_, Game>(
@ -179,16 +244,28 @@ async fn score_for_game_html(State(server_config): State<ServerState>, Path(game
.fetch_one(&*server_config.db_pool)
.await
.unwrap();
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 = 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))
let score_html = TeamGameStatsTemplate { teams: score };
let goal_details = get_box_score_from_game(&server_config.db_pool, &game).await.unwrap();
let goal_details_html = IndividualGamePointsTableTemplate { players: goal_details };
let box_score = get_goals_from_game(&server_config.db_pool, &game).await.unwrap();
let box_score_html = BoxScoreTemplate { goals: box_score };
let pbp_html = ShotsTableTemplate {
shots: pbp
};
let game_template = GameScorePageTemplate {
division,
game,
box_score: box_score_html,
team_stats: score_html,
individual_stats: goal_details_html,
play_by_play: pbp_html,
};
(StatusCode::OK, game_template)
}
/*
@ -203,6 +280,7 @@ macro_rules! insert {
}
}
}
*/
macro_rules! impl_all_query_types {
($ty:ident, $func_all:ident, $func_by_id:ident) => {
@ -241,5 +319,3 @@ impl_all_query_types!(
league_all,
league_id
);
*/

@ -7,15 +7,17 @@ pub trait TableName {
const TABLE_NAME: &'static str;
}
macro_rules! impl_table_name {
($ty:ident, $tname:expr) => {
($ty:ident, $tname:literal) => {
impl TableName for $ty {
const TABLE_NAME: &'static str = $tname;
}
}
}
#[derive(FromRow, Serialize, Deserialize, Debug)]
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)]
#[ormx(table = "leagues", id = id, insertable, deletable)]
pub struct League {
#[ormx(default)]
pub id: i32,
pub name: String,
#[serde(with = "ts_seconds")]
@ -23,7 +25,8 @@ pub struct League {
#[serde(with = "ts_seconds_option")]
pub end_date: Option<DateTime<Utc>>,
}
#[derive(FromRow, Serialize, Deserialize)]
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)]
#[ormx(table_name = "leagues", table = League, id = "id")]
pub struct NewLeague {
pub name: String,
#[serde(with = "ts_seconds")]
@ -32,72 +35,104 @@ pub struct NewLeague {
pub end_date: Option<DateTime<Utc>>,
}
#[derive(FromRow, Serialize, Deserialize)]
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)]
#[ormx(table = "divisions", id = id, insertable, deletable)]
pub struct Division {
#[ormx(default)]
pub id: i32,
pub name: String,
#[ormx(get_many(i32))]
pub league: i32,
}
#[derive(FromRow, Serialize, Deserialize)]
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)]
#[ormx(table_name = "divisions", table = Division, id = "id")]
pub struct NewDivision {
pub id: i32,
pub name: String,
pub league: i32,
}
#[derive(FromRow, Serialize, Deserialize)]
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)]
#[ormx(table = "teams", id = id, insertable, deletable)]
pub struct Team {
#[ormx(default)]
pub id: i32,
pub name: String,
pub division: i32,
pub image: Option<String>,
}
#[derive(FromRow, Serialize, Deserialize)]
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)]
#[ormx(table_name = "teams", table = Team, id = "id")]
pub struct NewTeam {
pub name: String,
pub division: i32,
}
#[derive(FromRow, Serialize, Deserialize)]
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)]
#[ormx(table = "players", id = id, insertable, deletable)]
pub struct Player {
#[ormx(default)]
pub id: i32,
pub name: String,
pub weight_kg: Option<i32>,
pub height_cm: Option<i32>,
}
#[derive(FromRow, Deserialize, Serialize)]
impl Player {
pub async fn from_name_case_insensitive(pool: &sqlx::PgPool, name: String) -> Option<Player> {
sqlx::query_as::<_, Player>("SELECT * FROM players WHERE REPLACE(UPPER(name), ' ', '-') LIKE UPPER($1);")
.bind(name)
.fetch_optional(pool)
.await
.unwrap()
}
}
#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Patch)]
#[ormx(table_name = "players", table = Player, id = "id")]
pub struct NewPlayer {
pub name: String,
pub weight_kg: Option<i32>,
pub height_cm: Option<i32>,
}
#[derive(FromRow, Deserialize, Serialize)]
#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)]
#[ormx(table = "shots", id = id, insertable, deletable)]
pub struct Shot {
#[ormx(default)]
pub id: i32,
pub shooter_team: i32,
pub shooter: i32,
pub goalie: i32,
pub assistant: Option<i32>,
pub game: i32,
pub period: i32,
pub period_time: i32,
pub video_timestamp: Option<i32>,
pub blocker: Option<i32>,
pub on_net: bool,
pub assistant_second: Option<i32>,
pub goal: bool,
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
}
#[derive(FromRow, Deserialize, Serialize)]
#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)]
#[ormx(table = "team_players", id = id, insertable, deletable)]
pub struct TeamPlayer {
#[ormx(default)]
pub id: i32,
pub team: i32,
pub player: i32,
pub position: i32,
}
#[derive(FromRow, Deserialize, Serialize)]
#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)]
#[ormx(table = "games", id = id, insertable, deletable)]
pub struct Game {
#[ormx(default)]
pub id: i32,
#[ormx(get_many(i32))]
pub division: i32,
pub name: String,
pub team_home: i32,
@ -126,6 +161,17 @@ mod tests {
Game,
};
#[test]
fn test_get_player_from_name() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let player = Player::from_name_case_insensitive(&pool, "tait-hoyem".to_string()).await;
assert!(player.is_some());
let player = player.unwrap();
assert_eq!(player.name, "Tait Hoyem");
})
}
/// 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.");

@ -1,14 +1,17 @@
use sqlx::FromRow;
use sqlx::PgPool;
use crate::model::Game;
use crate::model::{
Player,
Game,
League,
};
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,
pub struct TeamStats {
pub name: String,
pub goals: i64,
pub shots: i64,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
@ -23,58 +26,244 @@ pub struct Notification {
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct PlayerStats {
pub player_name: String,
pub 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#"
let query = r#"
SELECT
(
SELECT COUNT(id)
SELECT COUNT(shots.id)
FROM shots
JOIN periods ON periods.id=shots.period
WHERE shooter=players.id
AND goal=true
AND game=$1
AND periods.game=$1
) AS goals,
(
SELECT COUNT(id)
SELECT COUNT(shots.id)
FROM shots
WHERE assistant=players.id
AND goal=true
AND game=$1
JOIN periods ON periods.id=shots.period
WHERE (assistant=players.id
OR assistant_second=players.id)
AND goal=true
AND periods.game=$1
) AS assists,
(
SELECT COUNT(id)
SELECT COUNT(shots.id)
FROM shots
JOIN periods ON periods.id=shots.period
WHERE (assistant=players.id
OR assistant_second=players.id
OR shooter=players.id)
AND game=$1
AND goal=true
AND periods.game=$1
) AS points,
players.name AS player_name
players.name AS name
FROM players
JOIN shots ON shots.shooter=players.id OR shots.assistant=players.id
WHERE shots.game = $1
JOIN periods ON periods.id=shots.period
WHERE periods.game = $1
GROUP BY players.id
-- exclude players who do not have any points
-- NOTE: we can NOT use the aliased column "points" here, so we need to recalculate it.
-- This should not be a performance problem because the optimizer should deal with duplicate sub-queries.
HAVING
(
SELECT COUNT(shots.id)
FROM shots
JOIN periods ON periods.id=shots.period
WHERE (assistant=players.id
OR assistant_second=players.id
OR shooter=players.id)
AND goal=true
AND periods.game=$1
) > 0
ORDER BY
points DESC,
goals DESC,
players.name;
"#);
"#;
sqlx::query_as::<_, PlayerStats>(&query)
.bind(game.id)
.fetch_all(pool)
.await
}
pub async fn get_latest_league_for_player(pool: &PgPool, player: &Player) -> Result<Option<League>, sqlx::Error> {
let query =
r#"
SELECT leagues.*
FROM players
JOIN team_players ON team_players.player=players.id
JOIN teams ON teams.id=team_players.team
JOIN divisions ON divisions.id=teams.division
JOIN leagues ON leagues.id=divisions.league
WHERE players.id=$1
ORDER BY leagues.end_date DESC
LIMIT 1;
"#;
sqlx::query_as::<_, League>(&query)
.bind(player.id)
.fetch_optional(pool)
.await
}
pub async fn get_league_player_stats(pool: &PgPool, player: &Player, league: &League) -> Result<PlayerStats, sqlx::Error> {
let query = r#"
SELECT
(
SELECT COUNT(shots.id)
FROM shots
JOIN periods ON periods.id=shots.period
JOIN games ON games.id=periods.game
JOIN divisions ON divisions.id=games.division
JOIN leagues ON leagues.id=divisions.league
WHERE shots.goal=true
AND shots.shooter=players.id
AND leagues.id=$2
) AS goals,
(
SELECT COUNT(shots.id)
FROM shots
JOIN periods ON periods.id=shots.period
JOIN games ON games.id=periods.game
JOIN divisions ON divisions.id=games.division
JOIN leagues ON leagues.id=divisions.league
WHERE shots.goal=true
AND leagues.id=$2
AND (shots.assistant=players.id
OR shots.assistant_second=players.id)
) AS assists,
(
SELECT COUNT(shots.id)
FROM shots
JOIN periods ON periods.id=shots.period
JOIN games ON games.id=periods.game
JOIN divisions ON divisions.id=games.division
JOIN leagues ON leagues.id=divisions.league
WHERE shots.goal=true
AND leagues.id=$2
AND (shots.shooter=players.id
OR shots.assistant=players.id
OR shots.assistant_second=players.id)
) AS points,
players.name AS name
FROM players
WHERE id=$1;
"#;
sqlx::query_as::<_, PlayerStats>(&query)
.bind(player.id)
.bind(league.id)
.fetch_one(pool)
.await
}
pub async fn get_latest_stats(pool: &PgPool, player: &Player) -> Result<Vec<GoalDetails>, sqlx::Error> {
let query = r#"
SELECT
shots.shooter AS player_id,
shots.assistant AS first_assist_id,
shots.assistant_second AS second_assist_id,
(
SELECT name
FROM players
WHERE id=shots.shooter
) AS player_name,
(
SELECT name
FROM players
WHERE id=shots.assistant
) AS first_assist_name,
(
SELECT name
FROM players
WHERE id=shots.assistant_second
) AS second_assist_name,
(
SELECT player_number
FROM team_players
WHERE player=shots.shooter
AND team=shots.shooter_team
) AS player_number,
(
SELECT player_number
FROM team_players
WHERE player=shots.assistant
AND team=shots.shooter_team
) AS first_assist_number,
(
SELECT player_number
FROM team_players
WHERE player=shots.assistant_second
AND team=shots.shooter_team
) AS second_assist_number,
teams.name AS team_name,
teams.id AS team_id,
shots.shooter_team AS player_team,
shots.period_time AS time_remaining,
period_types.id AS period_id,
period_types.short_name AS period_short_name
FROM shots
JOIN periods ON shots.period=periods.id
JOIN period_types ON periods.period_type=period_types.id
JOIN teams ON shots.shooter_team=teams.id
WHERE shots.shooter=$1
ORDER BY
shots.created_at DESC,
periods.period_type DESC,
shots.period_time ASC
LIMIT 5;
"#;
let x =sqlx::query_as::<_, GoalDetails>(&query)
.bind(player.id);
//.fetch_all(pool)
//.await
x.fetch_all(pool).await
}
pub async fn get_all_player_stats(pool: &PgPool, player: &Player) -> Result<PlayerStats, sqlx::Error> {
let query =r#"
SELECT
(
SELECT COUNT(id)
FROM shots
WHERE shots.goal=true
AND shots.shooter=players.id
) AS goals,
(
SELECT COUNT(id)
FROM shots
WHERE shots.goal=true
AND (shots.assistant=players.id
OR shots.assistant_second=players.id)
) AS assists,
(
SELECT COUNT(id)
FROM shots
WHERE shots.goal=true
AND (shots.shooter=players.id
OR shots.assistant=players.id
OR shots.assistant_second=players.id)
) AS points,
players.name AS name
FROM players
WHERE id=$1;
"#;
sqlx::query_as::<_, PlayerStats>(&query)
.bind(player.id)
.fetch_one(pool)
.await
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct DetailGoals {
pub struct GoalDetails {
pub player_id: i32,
pub player_name: String,
pub number: i32,
pub player_number: i32,
pub team_name: String,
pub team_id: i32,
pub time_remaining: i32,
@ -87,43 +276,172 @@ pub struct DetailGoals {
pub second_assist_number: Option<i32>,
}
pub async fn get_goals_from_game(pool: &PgPool, game: &Game) -> Result<Vec<NiceGoals>, sqlx::Error> {
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct ShotDetails {
pub player_id: i32,
pub player_name: String,
pub player_number: i32,
pub team_name: String,
pub team_id: i32,
pub is_goal: bool,
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_score_from_game(pool: &PgPool, game: &Game) -> Result<Score, sqlx::Error> {
let query = format!(r#"
SELECT
pub async fn get_goals_from_game(pool: &PgPool, game: &Game) -> Result<Vec<GoalDetails>, sqlx::Error> {
sqlx::query_as::<_, GoalDetails>(
r#"
SELECT
shots.shooter AS player_id,
shots.assistant AS first_assist_id,
shots.assistant_second AS second_assist_id,
(
SELECT COUNT(id)
FROM shots
WHERE game=$1
AND goal=true
AND shooter_team=$2
) AS home,
SELECT name
FROM players
WHERE id=shots.shooter
) AS player_name,
(
SELECT COUNT(id)
FROM shots
WHERE game=$1
AND goal=true
AND shooter_team=$3
) AS away,
SELECT name
FROM players
WHERE id=shots.assistant
) AS first_assist_name,
(
SELECT name
FROM players
WHERE id=shots.assistant_second
) AS second_assist_name,
(
SELECT player_number
FROM team_players
WHERE player=shots.shooter
AND team=shots.shooter_team
) AS player_number,
(
SELECT player_number
FROM team_players
WHERE player=shots.assistant
AND team=shots.shooter_team
) AS first_assist_number,
(
SELECT player_number
FROM team_players
WHERE player=shots.assistant_second
AND team=shots.shooter_team
) AS second_assist_number,
teams.name AS team_name,
teams.id AS team_id,
shots.shooter_team AS player_team,
shots.period_time AS time_remaining,
period_types.id AS period_id,
period_types.short_name AS period_short_name
FROM shots
JOIN periods ON shots.period=periods.id
JOIN period_types ON periods.period_type=period_types.id
JOIN teams ON shots.shooter_team=teams.id
WHERE shots.goal=true
AND periods.game=$1
ORDER BY
periods.period_type ASC,
shots.period_time DESC;
"#)
.bind(game.id)
.fetch_all(pool)
.await
}
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,
(
SELECT name
FROM players
WHERE id=shots.shooter
) AS player_name,
(
SELECT name
FROM teams
WHERE id=$2
) AS home_name,
FROM players
WHERE id=shots.assistant
) AS first_assist_name,
(
SELECT name
FROM teams
WHERE id=$3
) AS away_name
FROM games;
FROM players
WHERE id=shots.assistant_second
) AS second_assist_name,
(
SELECT player_number
FROM team_players
WHERE player=shots.shooter
AND team=shots.shooter_team
) AS player_number,
(
SELECT player_number
FROM team_players
WHERE player=shots.assistant
AND team=shots.shooter_team
) AS first_assist_number,
(
SELECT player_number
FROM team_players
WHERE player=shots.assistant_second
AND team=shots.shooter_team
) AS second_assist_number,
teams.name AS team_name,
teams.id AS team_id,
shots.shooter_team AS player_team,
shots.period_time AS time_remaining,
period_types.id AS period_id,
period_types.short_name AS period_short_name
FROM shots
JOIN periods ON shots.period=periods.id
JOIN period_types ON periods.period_type=period_types.id
JOIN teams ON shots.shooter_team=teams.id
WHERE periods.game=$1
ORDER BY
periods.period_type ASC,
shots.period_time DESC;
"#)
.bind(game.id)
.fetch_all(pool)
.await
}
pub async fn get_score_from_game(pool: &PgPool, game: &Game) -> Result<Vec<TeamStats>, sqlx::Error> {
let query = format!(r#"
SELECT
(
SELECT COUNT(shots.id)
FROM shots
JOIN periods ON periods.id=shots.period
WHERE periods.game=$1
AND shots.goal=true
AND shots.shooter_team=teams.id
) AS goals,
(
SELECT COUNT(shots.id)
FROM shots
JOIN periods ON periods.id=shots.period
WHERE periods.game=$1
AND shooter_team=teams.id
) AS shots,
teams.name AS name
FROM games
JOIN teams ON teams.id=games.team_home OR teams.id=games.team_away
WHERE games.id=$1;
"#);
sqlx::query_as::<_, Score>(&query)
sqlx::query_as::<_, TeamStats>(&query)
.bind(game.id)
.bind(game.team_home)
.bind(game.team_away)
.fetch_one(pool)
.fetch_all(pool)
.await
}
@ -139,7 +457,7 @@ SELECT
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
WHERE (assistant=players.id OR assistant_second=players.id)
AND goal=true
) AS assists,
(
@ -148,7 +466,7 @@ SELECT
WHERE assistant=players.id
OR shooter=players.id
) AS points,
players.name AS player_name
players.name AS name
FROM players
ORDER BY
points DESC,
@ -166,19 +484,101 @@ mod tests {
use std::env;
use crate::model::{
Game,
Player,
League,
};
use crate::views::{
Notification,
get_player_stats_overview,
get_score_from_game,
get_goals_from_game,
get_box_score_from_game,
get_latest_league_for_player,
get_league_player_stats,
get_all_player_stats,
get_latest_stats,
};
#[test]
fn get_latest_stats_of_player() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let player = sqlx::query_as::<_, Player>("SELECT * FROM id=2;")
.fetch_one(&pool)
.await
.unwrap();
let latest = get_latest_stats(&pool, &player)
.await
.unwrap();
})
}
#[test]
fn check_all_player_stats() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let player = sqlx::query_as::<_, Player>("SELECT * FROM players WHERE id=2;")
.fetch_one(&pool)
.await
.unwrap();
let stats = get_all_player_stats(&pool, &player).await.unwrap();
assert_eq!(stats.name, "Hillary Scanlon");
})
}
#[test]
fn check_league_player_stats() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let league = sqlx::query_as::<_, League>("SELECT * FROM leagues WHERE id=1;")
.fetch_one(&pool)
.await
.unwrap();
let player = sqlx::query_as::<_, Player>("SELECT * FROM players WHERE id=2;")
.fetch_one(&pool)
.await
.unwrap();
let stats = get_league_player_stats(&pool, &player, &league).await.unwrap();
assert_eq!(stats.name, "Hillary Scanlon");
})
}
#[test]
fn check_latest_league_for_player() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let player = sqlx::query_as::<_, Player>("SELECT * FROM players WHERE id=5")
.fetch_one(&pool)
.await
.unwrap();
let league = get_latest_league_for_player(&pool, &player)
.await
.unwrap()
.unwrap();
assert_eq!(league.id, 1);
})
}
#[test]
fn check_score_details_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_goals_from_game(&pool, &game)
.await
.unwrap();
println!("{scores:?}");
})
}
#[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;")
let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id=4;")
.fetch_one(&pool)
.await
.unwrap();
@ -186,7 +586,12 @@ mod tests {
.await
.unwrap();
println!("{scores:?}");
assert_eq!(scores.get(0).unwrap().player_name, "Brian MacLean");
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.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.");
})
}
@ -201,8 +606,8 @@ mod tests {
let score = get_score_from_game(&pool, &game)
.await
.unwrap();
assert_eq!(score.away, 1);
assert_eq!(score.home, 1);
assert_eq!(score.get(0).unwrap().goals, 1);
assert_eq!(score.get(1).unwrap().goals, 1);
})
}
@ -228,13 +633,14 @@ SELECT
positions.name AS position,
team_players.player_number AS scorer_number,
shots.period_time AS period_time_left,
periods.name AS period_name
period_types.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 period_types ON period_types.id=periods.period_type
JOIN positions ON positions.id=team_players.position;
"#;
let result = sqlx::query_as::<_, Notification>(query)

@ -0,0 +1,6 @@
<h1 id="first-heading">Divisions for the {{ league.name }}</h1>
<ul aria-labelledby="first-heading">
{% for division in divisions %}
<li><a href="/division/{{ division.id }}/">{{ division.name }}</a></li>
{% endfor %}
</ul>

@ -0,0 +1,10 @@
<h1 id="games">Games for {{ division.name }}</h1>
{% if games.len() > 0 %}
<ol aria-labelledby="games">
{% for game in games %}
<li><a href="/game/{{ game.id }}/">{{ game.name }}</a></li>
{% endfor %}
</ol>
{% else %}
<p>No games have been recorded.</p>
{% endif %}

@ -0,0 +1,9 @@
<h1>{{ game.name }} of the {{ division.name }}</h1>
<h2>Team</h2>
{{ team_stats|safe }}
<h2>Individual</h2>
{{ individual_stats|safe }}
<h2>Box Score</h2>
{{ box_score|safe }}
<h2>Play-by-Play</h2>
{{ play_by_play|safe }}

@ -0,0 +1,2 @@
Hello, World!
My name is {{ name }} and I am {{ years }} years old.

@ -0,0 +1,6 @@
<h1 id="leagues">IBIHF Leagues</h1>
<ol aria-labelledby="leagues">
{% for league in leagues %}
<li><a href="/league/{{ league.id }}/">{{ league.name }}</a></li>
{% endfor %}
</ol>

@ -0,0 +1,36 @@
<table>
<thead>
<th>Scorer</td>
<th>Team</th>
<th>#</th>
<th>Period</th>
<th>Time</th>
<th>Assist</th>
<th>Secondary Assist</th>
</thead>
<tbody>
{% for goal in goals %}
<tr>
<td>{{ goal.player_name }}</td>
<td>{{ goal.team_name }}</td>
<td>{{ goal.player_number }}</td>
<td>{{ goal.period_short_name }}</td>
<td>{{ goal.time_remaining|seconds_as_time }}</td>
<td>
{% if goal.first_assist_name.is_some() %}
{{ goal.first_assist_name.as_ref().unwrap() }}
{% else %}
unassisted
{% endif %}
</td>
<td>
{% if goal.second_assist_name.is_some() %}
{{ goal.second_assist_name.as_ref().unwrap() }}
{% else %}
N/A
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

@ -0,0 +1,20 @@
<table>
<thead>
<tr>
<th>Name</th>
<th>Points</th>
<th>Goals</th>
<th>Assists</th>
</tr>
</thead>
<tbody>
{% for player in players %}
<tr>
<td>{{ player.name }}</td>
<td>{{ player.points }}</td>
<td>{{ player.goals }}</td>
<td>{{ player.assists }}</td>
</tr>
{% endfor %}
</tbody>
</table>

@ -0,0 +1,48 @@
<table>
<thead>
<th>Shooter</td>
<th>Team</th>
<th>#</th>
<th>Type</th>
<th>Period</th>
<th>Time</th>
<th>Assist</th>
<th>Secondary Assist</th>
</thead>
<tbody>
{% for shot in shots %}
<tr>
<td>{{ shot.player_name }}</td>
<td>{{ shot.team_name }}</td>
<td>{{ shot.player_number }}</td>
<td>
{% if shot.is_goal %}
Goal
{% else %}
Shot
{% endif %}
</td>
<td>{{ shot.period_short_name }}</td>
<td>{{ shot.time_remaining|seconds_as_time }}</td>
<td>
{% if shot.is_goal %}
{% if shot.first_assist_name.is_some() %}
{{ shot.first_assist_name.as_ref().unwrap() }}
{% else %}
unassisted
{% endif %}
{% else %}
N/A
{% endif %}
</td>
<td>
{% if shot.second_assist_name.is_some() %}
{{ shot.second_assist_name.as_ref().unwrap() }}
{% else %}
N/A
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

@ -0,0 +1,18 @@
<table>
<thead>
<tr>
<th>Team</th>
<th>Goals</th>
<th>Shots</th>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{ team.name }}</td>
<td>{{ team.goals }}</td>
<td>{{ team.shots }}</td>
</tr>
{% endfor %}
</tbody>
</table>

@ -0,0 +1,17 @@
<body>
<h1>{{ player.name }}</h1>
<h2>Latest Competition: {{ league.name }}</h2>
<label for="league_points">Points</label>
<span id="league_points">{{ league_stats.points }}</span>
<label for="league_goals">Goals</label>
<span id="league_goals">{{ league_stats.goals }}</span>
<label for="league_goals">Assists</label>
<span id="league_goals">{{ league_stats.goals }}</span>
<h2>Lifetime Stats</h2>
<label for="lfietime_points">Points</label>
<span id="lfietime_points">{{ lifetime_stats.points }}</span>
<label for="lfietime_goals">Goals</label>
<span id="lfietime_goals">{{ lifetime_stats.goals }}</span>
<label for="lfietime_goals">Assists</label>
<span id="lfietime_goals">{{ lifetime_stats.goals }}</span>
</body>
Loading…
Cancel
Save