master
Tait Hoyem 1 year ago
parent 0d2e4f3574
commit b0b0e8c33d

@ -1,8 +1,9 @@
-- Add up migration script here -- Add up migration script here
CREATE TABLE IF NOT EXISTS games ( CREATE TABLE IF NOT EXISTS games (
id SERIAL PRIMARY KEY NOT NULL, id SERIAL PRIMARY KEY NOT NULL,
-- a possibly null name for the game; this allows there to be special names like "Gold Medal Game" -- 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
name VARCHAR(255), -- 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) -- what divison is the game a part of (usefl for stats)
division INTEGER NOT NULL, division INTEGER NOT NULL,
team_home 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, id SERIAL PRIMARY KEY NOT NULL,
-- "first", "second", "third", "second overtime", "shootout" -- "first", "second", "third", "second overtime", "shootout"
name VARCHAR(32) NOT NULL, name VARCHAR(32) NOT NULL,
-- "1", "2", "3", "OT", "[2-9]OT", "SO" -- "1", "2", "3", "OT", "[2-9]OT", "SO"
-- technically 10+OT would not work, but this should be rare enough to not worry about. -- 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, shooter INTEGER NOT NULL,
-- which player was the goalie -- which player was the goalie
goalie INTEGER NOT NULL, goalie INTEGER NOT NULL,
-- which game was this a part of
game INTEGER NOT NULL,
-- which period did the shot happen in -- which period did the shot happen in
period INTEGER NOT NULL, period INTEGER NOT NULL,
-- when did the shot happen relative to the beginning of the period -- 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) -- if applicable, set assistant(s)
assistant INTEGER, assistant INTEGER,
assistant_second INTEGER, assistant_second INTEGER,
-- when was the record created
created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp,
-- was the shooter a real player -- was the shooter a real player
CONSTRAINT shooter_fk CONSTRAINT shooter_fk
FOREIGN KEY(shooter) FOREIGN KEY(shooter)
@ -55,12 +55,7 @@ CREATE TABLE IF NOT EXISTS shots (
FOREIGN KEY(shooter_team) FOREIGN KEY(shooter_team)
REFERENCES teams(id) REFERENCES teams(id)
ON DELETE RESTRICT, ON DELETE RESTRICT,
-- is the game a real game -- is the period references a real period
CONSTRAINT game_fk
FOREIGN KEY(game)
REFERENCES games(id)
ON DELETE RESTRICT,
-- is the period refgerences a real period type
CONSTRAINT period_fk CONSTRAINT period_fk
FOREIGN KEY(period) FOREIGN KEY(period)
REFERENCES periods(id) 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 db;
mod model; mod model;
mod views; mod views;
mod filters;
use crate::model::{ use crate::model::{
TableName,
League, League,
Team, Team,
Division, Division,
@ -13,14 +13,24 @@ use crate::model::{
Game, Game,
}; };
use views::{ use views::{
GoalDetails,
PlayerStats,
TeamStats,
ShotDetails,
get_score_from_game, get_score_from_game,
get_box_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::{ use sqlx::{
Postgres, Postgres,
Pool, Pool,
}; };
use ormx::Table;
use axum::{ use axum::{
Router, Router,
http::StatusCode, http::StatusCode,
@ -38,6 +48,78 @@ use axum::{
use axum_macros::debug_handler; use axum_macros::debug_handler;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; 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)] #[derive(Clone)]
pub struct ServerState { pub struct ServerState {
@ -52,9 +134,12 @@ async fn main() {
}; };
let router = Router::new() let router = Router::new()
.route("/", get(league_html)) .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("/division/:id/", get(games_for_division_html))
.route("/game/:id/", get(score_for_game_html)) .route("/game/:id/", get(score_for_game_html))
.route("/player/:name/", get(player_from_name))
.with_state(state); .with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 8000)); let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
println!("Listening on {}", addr); println!("Listening on {}", addr);
@ -64,39 +149,38 @@ async fn main() {
.unwrap(); .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> { async fn player_from_name(State(server_config): State<ServerState>, Path(name): Path<String>) -> impl IntoResponse {
sqlx::query_as::<_, T>( let player = Player::from_name_case_insensitive(&server_config.db_pool, name)
&format!("SELECT * FROM {};", <T as TableName>::TABLE_NAME) .await
) .unwrap();
.fetch_all(pool) let latest_league = get_latest_league_for_player(&server_config.db_pool, &player)
.await .await
} .unwrap()
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> { .unwrap();
sqlx::query_as::<_, T>( let latest_league_stats = get_league_player_stats(&server_config.db_pool, &player, &latest_league)
&format!("SELECT * FROM {} WHERE id = $1;", <T as TableName>::TABLE_NAME) .await
) .unwrap();
.bind(id) let lifetime_stats = get_all_player_stats(&server_config.db_pool, &player)
.fetch_optional(pool) .await
.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> { async fn test_template<'a>() -> HelloTemplate<'a> {
let query = sql_builder::SqlBuilder::insert_into(<T as TableName>::TABLE_NAME) HelloTemplate { name: "Tait", years: 24 }
.values(())
.sql().unwrap();
sqlx::query(
&query
)
.execute(pool)
.await
} }
*/
macro_rules! get_all { macro_rules! get_all {
($crud_struct:ident, $func_name:ident) => { ($crud_struct:ident, $func_name:ident) => {
#[debug_handler] #[debug_handler]
async fn $func_name(State(server_config): State<ServerState>) -> impl IntoResponse { 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 .await
.unwrap(); .unwrap();
(StatusCode::OK, Json(cruder)) (StatusCode::OK, Json(cruder))
@ -107,7 +191,7 @@ macro_rules! get_by_id {
($crud_struct:ident, $func_name:ident) => { ($crud_struct:ident, $func_name:ident) => {
#[debug_handler] #[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 = get_by_id::<$crud_struct>(&server_config.db_pool, id) let cruder = $crud_struct::get(&*server_config.db_pool, id)
.await .await
.unwrap(); .unwrap();
(StatusCode::OK, Json(cruder)) (StatusCode::OK, Json(cruder))
@ -116,60 +200,41 @@ macro_rules! get_by_id {
} }
async fn league_html(State(server_config): State<ServerState>) -> impl IntoResponse { async fn league_html(State(server_config): State<ServerState>) -> impl IntoResponse {
let leagues_html = get_all::<League>(&server_config.db_pool).await let leagues = League::all(&*server_config.db_pool)
.unwrap() .await
.iter() .unwrap();
.map(|league| { let leagues_template = LeagueListTemplate {
format!( leagues
"<li><a href=\"{1}\">{0}</a></li>", };
league.name, (StatusCode::OK, leagues_template)
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 { 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") let league = League::get(&*server_config.db_pool, league_id)
.bind(league_id)
.fetch_all(&*server_config.db_pool)
.await .await
.unwrap() .unwrap();
.iter() let divisions = Division::by_league(&*server_config.db_pool, league_id)
.map(|division| { .await
format!( .unwrap();
"<li><a href=\"{1}\">{0}</a></li>", let html = DivisionListTemplate {
division.name, league,
format!("/division/{}/", division.id), divisions
) };
}) (StatusCode::OK, html)
.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 { 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") let division = Division::get(&*server_config.db_pool, division_id)
.bind(division_id)
.fetch_all(&*server_config.db_pool)
.await .await
.unwrap() .unwrap();
.iter() let games = Game::by_division(&*server_config.db_pool, division.id)
.map(|game| { .await
format!( .unwrap();
"<li><a href=\"{1}\">{0}</a></li>", let games_template = GameListTemplate {
game.name, division,
format!("/game/{}/", game.id), games
) };
}) (StatusCode::OK, games_template)
.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 { async fn score_for_game_html(State(server_config): State<ServerState>, Path(game_id): Path<i32>) -> impl IntoResponse {
let game = sqlx::query_as::<_, Game>( 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) .fetch_one(&*server_config.db_pool)
.await .await
.unwrap(); .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 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() let score_html = TeamGameStatsTemplate { teams: score };
.iter() let goal_details = get_box_score_from_game(&server_config.db_pool, &game).await.unwrap();
.map(|player_stats| { let goal_details_html = IndividualGamePointsTableTemplate { players: goal_details };
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) let box_score = get_goals_from_game(&server_config.db_pool, &game).await.unwrap();
}) let box_score_html = BoxScoreTemplate { goals: box_score };
.collect::<Vec<String>>() let pbp_html = ShotsTableTemplate {
.join(""); shots: pbp
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 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 { macro_rules! impl_all_query_types {
($ty:ident, $func_all:ident, $func_by_id:ident) => { ($ty:ident, $func_all:ident, $func_by_id:ident) => {
@ -241,5 +319,3 @@ impl_all_query_types!(
league_all, league_all,
league_id league_id
); );
*/

@ -7,15 +7,17 @@ pub trait TableName {
const TABLE_NAME: &'static str; const TABLE_NAME: &'static str;
} }
macro_rules! impl_table_name { macro_rules! impl_table_name {
($ty:ident, $tname:expr) => { ($ty:ident, $tname:literal) => {
impl TableName for $ty { impl TableName for $ty {
const TABLE_NAME: &'static str = $tname; 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 { pub struct League {
#[ormx(default)]
pub id: i32, pub id: i32,
pub name: String, pub name: String,
#[serde(with = "ts_seconds")] #[serde(with = "ts_seconds")]
@ -23,7 +25,8 @@ pub struct League {
#[serde(with = "ts_seconds_option")] #[serde(with = "ts_seconds_option")]
pub end_date: Option<DateTime<Utc>>, 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 struct NewLeague {
pub name: String, pub name: String,
#[serde(with = "ts_seconds")] #[serde(with = "ts_seconds")]
@ -32,72 +35,104 @@ pub struct NewLeague {
pub end_date: Option<DateTime<Utc>>, 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 { pub struct Division {
#[ormx(default)]
pub id: i32, pub id: i32,
pub name: String, pub name: String,
#[ormx(get_many(i32))]
pub league: 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 struct NewDivision {
pub id: i32,
pub name: String, pub name: String,
pub league: i32, pub league: i32,
} }
#[derive(FromRow, Serialize, Deserialize)] #[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)]
#[ormx(table = "teams", id = id, insertable, deletable)]
pub struct Team { pub struct Team {
#[ormx(default)]
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub division: i32, pub division: i32,
pub image: Option<String>, 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 struct NewTeam {
pub name: String, pub name: String,
pub division: i32, pub division: i32,
} }
#[derive(FromRow, Serialize, Deserialize)] #[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)]
#[ormx(table = "players", id = id, insertable, deletable)]
pub struct Player { pub struct Player {
#[ormx(default)]
pub id: i32, pub id: i32,
pub name: String, pub name: String,
pub weight_kg: Option<i32>, pub weight_kg: Option<i32>,
pub height_cm: 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 struct NewPlayer {
pub name: String, pub name: String,
pub weight_kg: Option<i32>, pub weight_kg: Option<i32>,
pub height_cm: 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 { pub struct Shot {
#[ormx(default)]
pub id: i32, pub id: i32,
pub shooter_team: i32, pub shooter_team: i32,
pub shooter: i32,
pub goalie: i32, pub goalie: i32,
pub assistant: Option<i32>, pub assistant: Option<i32>,
pub game: i32,
pub period: i32, pub period: i32,
pub period_time: i32, pub period_time: i32,
pub video_timestamp: Option<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 { pub struct TeamPlayer {
#[ormx(default)]
pub id: i32, pub id: i32,
pub team: i32, pub team: i32,
pub player: i32, pub player: i32,
pub position: 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 { pub struct Game {
#[ormx(default)]
pub id: i32, pub id: i32,
#[ormx(get_many(i32))]
pub division: i32, pub division: i32,
pub name: String, pub name: String,
pub team_home: i32, pub team_home: i32,
@ -126,6 +161,17 @@ mod tests {
Game, 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. /// A simple function to connect to the database.
async fn db_connect() -> sqlx::PgPool { 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.");

@ -1,14 +1,17 @@
use sqlx::FromRow; use sqlx::FromRow;
use sqlx::PgPool; use sqlx::PgPool;
use crate::model::Game; use crate::model::{
Player,
Game,
League,
};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct Score { pub struct TeamStats {
pub home: i64, pub name: String,
pub home_name: String, pub goals: i64,
pub away: i64, pub shots: i64,
pub away_name: String,
} }
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
@ -23,58 +26,244 @@ pub struct Notification {
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct PlayerStats { pub struct PlayerStats {
pub player_name: String, pub name: String,
pub goals: i64, pub goals: i64,
pub assists: i64, pub assists: i64,
pub points: i64, pub points: i64,
} }
pub async fn get_box_score_from_game(pool: &PgPool, game: &Game) -> Result<Vec<PlayerStats>, sqlx::Error> { 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
( (
SELECT COUNT(id) SELECT COUNT(shots.id)
FROM shots FROM shots
JOIN periods ON periods.id=shots.period
WHERE shooter=players.id WHERE shooter=players.id
AND goal=true AND goal=true
AND game=$1 AND periods.game=$1
) AS goals, ) AS goals,
( (
SELECT COUNT(id) SELECT COUNT(shots.id)
FROM shots FROM shots
WHERE assistant=players.id JOIN periods ON periods.id=shots.period
AND goal=true WHERE (assistant=players.id
AND game=$1 OR assistant_second=players.id)
AND goal=true
AND periods.game=$1
) AS assists, ) AS assists,
( (
SELECT COUNT(id) SELECT COUNT(shots.id)
FROM shots FROM shots
JOIN periods ON periods.id=shots.period
WHERE (assistant=players.id WHERE (assistant=players.id
OR assistant_second=players.id
OR shooter=players.id) OR shooter=players.id)
AND game=$1 AND goal=true
AND periods.game=$1
) AS points, ) AS points,
players.name AS player_name players.name AS name
FROM players FROM players
JOIN shots ON shots.shooter=players.id OR shots.assistant=players.id 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 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 ORDER BY
points DESC, points DESC,
goals DESC, goals DESC,
players.name; players.name;
"#); "#;
sqlx::query_as::<_, PlayerStats>(&query) sqlx::query_as::<_, PlayerStats>(&query)
.bind(game.id) .bind(game.id)
.fetch_all(pool) .fetch_all(pool)
.await .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)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct DetailGoals { pub struct GoalDetails {
pub player_id: i32, pub player_id: i32,
pub player_name: String, pub player_name: String,
pub number: i32, pub player_number: i32,
pub team_name: String, pub team_name: String,
pub team_id: i32, pub team_id: i32,
pub time_remaining: i32, pub time_remaining: i32,
@ -87,43 +276,172 @@ pub struct DetailGoals {
pub second_assist_number: Option<i32>, 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> { pub async fn get_goals_from_game(pool: &PgPool, game: &Game) -> Result<Vec<GoalDetails>, sqlx::Error> {
let query = format!(r#" sqlx::query_as::<_, GoalDetails>(
SELECT r#"
SELECT
shots.shooter AS player_id,
shots.assistant AS first_assist_id,
shots.assistant_second AS second_assist_id,
( (
SELECT COUNT(id) SELECT name
FROM shots FROM players
WHERE game=$1 WHERE id=shots.shooter
AND goal=true ) AS player_name,
AND shooter_team=$2
) AS home,
( (
SELECT COUNT(id) SELECT name
FROM shots FROM players
WHERE game=$1 WHERE id=shots.assistant
AND goal=true ) AS first_assist_name,
AND shooter_team=$3 (
) AS away, 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 SELECT name
FROM teams FROM players
WHERE id=$2 WHERE id=shots.assistant
) AS home_name, ) AS first_assist_name,
( (
SELECT name SELECT name
FROM teams FROM players
WHERE id=$3 WHERE id=shots.assistant_second
) AS away_name ) AS second_assist_name,
FROM games; (
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.id)
.bind(game.team_home) .fetch_all(pool)
.bind(game.team_away)
.fetch_one(pool)
.await .await
} }
@ -139,7 +457,7 @@ SELECT
( (
SELECT COUNT(id) SELECT COUNT(id)
FROM shots FROM shots
WHERE assistant=players.id WHERE (assistant=players.id OR assistant_second=players.id)
AND goal=true AND goal=true
) AS assists, ) AS assists,
( (
@ -148,7 +466,7 @@ SELECT
WHERE assistant=players.id WHERE assistant=players.id
OR shooter=players.id OR shooter=players.id
) AS points, ) AS points,
players.name AS player_name players.name AS name
FROM players FROM players
ORDER BY ORDER BY
points DESC, points DESC,
@ -166,19 +484,101 @@ mod tests {
use std::env; use std::env;
use crate::model::{ use crate::model::{
Game, Game,
Player,
League,
}; };
use crate::views::{ use crate::views::{
Notification, Notification,
get_player_stats_overview, get_player_stats_overview,
get_score_from_game, get_score_from_game,
get_goals_from_game,
get_box_score_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] #[test]
fn check_box_score_from_game() { fn check_box_score_from_game() {
tokio_test::block_on(async move{ tokio_test::block_on(async move{
let pool = db_connect().await; 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) .fetch_one(&pool)
.await .await
.unwrap(); .unwrap();
@ -186,7 +586,12 @@ mod tests {
.await .await
.unwrap(); .unwrap();
println!("{scores:?}"); 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) let score = get_score_from_game(&pool, &game)
.await .await
.unwrap(); .unwrap();
assert_eq!(score.away, 1); assert_eq!(score.get(0).unwrap().goals, 1);
assert_eq!(score.home, 1); assert_eq!(score.get(1).unwrap().goals, 1);
}) })
} }
@ -228,13 +633,14 @@ SELECT
positions.name AS position, positions.name AS position,
team_players.player_number AS scorer_number, team_players.player_number AS scorer_number,
shots.period_time AS period_time_left, shots.period_time AS period_time_left,
periods.name AS period_name period_types.name AS period_name
FROM FROM
shots shots
JOIN teams ON teams.id=shots.shooter_team JOIN teams ON teams.id=shots.shooter_team
JOIN players ON players.id=shots.shooter JOIN players ON players.id=shots.shooter
JOIN team_players ON team_players.player=players.id AND team_players.team=teams.id JOIN team_players ON team_players.player=players.id AND team_players.team=teams.id
JOIN periods ON periods.id=shots.period 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; JOIN positions ON positions.id=team_players.position;
"#; "#;
let result = sqlx::query_as::<_, Notification>(query) 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