Almost done migrations to new, separated *_names tables; need more tests

master
Tait Hoyem 1 year ago
parent 6c628d24de
commit 3f8f7e33c5

@ -1,7 +1,22 @@
use darling::FromDeriveInput; use darling::FromDeriveInput;
use proc_macro::{self, TokenStream}; use proc_macro::{self, TokenStream};
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote; use quote::quote;
use syn::{parse_macro_input, DeriveInput}; use syn::{parse_macro_input, DeriveInput, Data, Field, Attribute, Ident};
fn matching_attr_map(attr: &Attribute, attr_name: &str) -> bool {
if let Ok(syn::Meta::List(meta_list)) = attr.parse_meta() {
return meta_list.path.is_ident("table_names")
&& meta_list.nested.iter().any(|nested_meta| {
if let syn::NestedMeta::Meta(syn::Meta::Path(path)) = nested_meta {
path.is_ident(attr_name)
} else {
false
}
});
}
false
}
#[derive(FromDeriveInput, Default)] #[derive(FromDeriveInput, Default)]
#[darling(default, attributes(urls))] #[darling(default, attributes(urls))]
@ -40,12 +55,61 @@ struct TableNameOpts {
name_table_name_fk: String, name_table_name_fk: String,
} }
fn get_map_filter(field: &Field) -> Option<String> {
let name = &field.ident.as_ref().unwrap();
if field.attrs.iter().any(|attr| attr.path.is_ident("get")) {
Some(name.to_string())
} else {
None
}
}
fn get_many_map_filter(field: &Field) -> Option<String> {
let name = &field.ident.as_ref().unwrap();
if field.attrs.iter().any(|attr| matching_attr_map(attr, "get_many")) {
Some(name.to_string())
} else {
None
}
}
#[proc_macro_derive(NameTableName, attributes(table_names))] #[proc_macro_derive(NameTableName, attributes(table_names))]
pub fn derive_get(input: TokenStream) -> TokenStream { pub fn derive_get(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input); let input = parse_macro_input!(input);
let opts = TableNameOpts::from_derive_input(&input).expect("Wrong options"); let opts = TableNameOpts::from_derive_input(&input).expect("Wrong options");
let DeriveInput { ident, .. } = input; let DeriveInput { ident, .. } = input;
let fields = match input.data {
Data::Struct(ref data) => &data.fields,
_ => panic!("MyDerive only supports structs"),
};
let by_many_names: Vec<String> = fields.iter().filter_map(get_many_map_filter).collect();
let by_many_funcs: Vec<TokenStream2> = by_many_names.iter()
.map(|name| {
let query = format!(r#"
SELECT
{0}.*,
{1}(id, $2) AS name
FROM {0}
WHERE {name} = $1;
"#, opts.table_name, opts.name_func);
let method = Ident::new(&format!("by_{}", name), Span::call_site());
let id_name = Ident::new(&format!("{}_id", name), Span::call_site());
quote! {
pub async fn #method(pool: &sqlx::PgPool, #id_name: i32, lang: i32) -> Result<Vec<Self>, sqlx::Error> {
sqlx::query_as!(
Self,
#query,
#id_name, lang
)
.fetch_all(pool)
.await
}
}
}.into())
.collect();
let table_name = opts.table_name; let table_name = opts.table_name;
let name_table_name = opts.name_table_name; let name_table_name = opts.name_table_name;
let name_table_name_fk = opts.name_table_name_fk; let name_table_name_fk = opts.name_table_name_fk;
@ -56,7 +120,7 @@ pub fn derive_get(input: TokenStream) -> TokenStream {
const NAME_TABLE_FK_NAME: &'static str = #name_table_name_fk; const NAME_TABLE_FK_NAME: &'static str = #name_table_name_fk;
}; };
let query = format!(r#" let get_query = format!(r#"
SELECT SELECT
{0}.*, {0}.*,
{1}({0}.id, $2) AS name {1}({0}.id, $2) AS name
@ -65,15 +129,33 @@ WHERE {0}.id = $1;"#,
table_name, table_name,
name_func, name_func,
); );
let all_query = format!(r#"
SELECT
{0}.*,
{1}({0}.id, $1) AS name
FROM {0}"#,
table_name,
name_func,
);
let output = quote! { let output = quote! {
impl NameTableName for #ident { impl NameTableName for #ident {
#answer #answer
} }
impl #ident { impl #ident {
#(#by_many_funcs)*
pub async fn all(pool: &sqlx::PgPool, lang: i32) -> Result<Vec<Self>, sqlx::Error> {
sqlx::query_as!(
#ident,
#all_query,
lang
)
.fetch_all(pool)
.await
}
pub async fn get(pool: &sqlx::PgPool, id: i32, lang: i32) -> Result<Option<Self>, sqlx::Error> { pub async fn get(pool: &sqlx::PgPool, id: i32, lang: i32) -> Result<Option<Self>, sqlx::Error> {
sqlx::query_as!( sqlx::query_as!(
#ident, #ident,
#query, #get_query,
id, lang id, lang
) )
.fetch_optional(pool) .fetch_optional(pool)

@ -1,8 +1,75 @@
use crate::{
Player,
ShotDetails,
GoalDetails,
SupportedLanguage,
};
pub fn seconds_as_time(secs: &i32) -> ::askama::Result<String> { pub fn seconds_as_time(secs: &i32) -> ::askama::Result<String> {
let minutes = secs / 60; let minutes = secs / 60;
let seconds = secs % 60; let seconds = secs % 60;
Ok(format!("{}:{}", minutes, seconds)) Ok(format!("{}:{}", minutes, seconds))
} }
pub fn player_name(player: &Player) -> ::askama::Result<String> {
Ok(format!("{} {}", initials(&player.first_names)?, &player.last_name))
}
pub fn goal_player_name(goal: &GoalDetails) -> ::askama::Result<String> {
Ok(format!("{} {}", initials(&goal.player_first_names)?, &goal.player_last_name))
}
pub fn goal_assist_name(goal: &GoalDetails, lang: &SupportedLanguage) -> ::askama::Result<String> {
let initials = match goal.first_assist_first_names {
Some(ref f_names) => initials(f_names)?,
None => return Ok(lang.lookup("not-applicable")),
};
let last_name = match goal.first_assist_last_name {
Some(ref l_name) => l_name,
None => return Ok(lang.lookup("not-applicable")),
};
Ok(format!("{} {}", initials, last_name))
}
pub fn shot_assist_name(goal: &ShotDetails, lang: &SupportedLanguage) -> ::askama::Result<String> {
let initials = match goal.first_assist_first_names {
Some(ref f_names) => initials(&f_names)?,
None => return Ok(lang.lookup("not-applicable")),
};
let last_name = match goal.first_assist_last_name {
Some(ref l_name) => l_name,
None => return Ok(lang.lookup("not-applicable")),
};
Ok(format!("{} {}", initials, last_name))
}
pub fn goal_second_assist_name(goal: &GoalDetails, lang: &SupportedLanguage) -> ::askama::Result<String> {
let initials = match goal.second_assist_first_names {
Some(ref f_names) => initials(f_names)?,
None => return Ok(lang.lookup("not-applicable")),
};
let last_name = match goal.second_assist_last_name {
Some(ref l_name) => l_name,
None => return Ok(lang.lookup("not-applicable")),
};
Ok(format!("{} {}", initials, last_name))
}
pub fn shot_second_assist_name(goal: &ShotDetails, lang: &SupportedLanguage) -> ::askama::Result<String> {
let initials = match goal.second_assist_first_names {
Some(ref f_names) => initials(f_names)?,
None => return Ok(lang.lookup("not-applicable")),
};
let last_name = match goal.second_assist_last_name {
Some(ref l_name) => l_name,
None => return Ok(lang.lookup("not-applicable")),
};
Ok(format!("{} {}", initials, last_name))
}
pub fn shot_player_name(shot: &ShotDetails) -> ::askama::Result<String> {
Ok(format!("{} {}", initials(&shot.player_first_names)?, &shot.player_last_name))
}
pub fn initials(first_names: &str) -> ::askama::Result<String> {
Ok(first_names
.split_whitespace()
.map(|name| &name[0..1])
.collect::<Vec<_>>()
.join("."))
}
pub fn nullable<T: std::fmt::Display>(ot: &Option<T>) -> ::askama::Result<String> { pub fn nullable<T: std::fmt::Display>(ot: &Option<T>) -> ::askama::Result<String> {
match ot { match ot {
Some(t) => Ok(format!("{}", t)), Some(t) => Ok(format!("{}", t)),

@ -113,6 +113,7 @@ struct BoxScoreTemplate<'a> {
#[locale] #[locale]
locale: Locale<'a>, locale: Locale<'a>,
goals: Vec<GoalDetails>, goals: Vec<GoalDetails>,
lang: SupportedLanguage,
} }
#[derive(Template)] #[derive(Template)]
@ -187,6 +188,7 @@ struct ShotsTableTemplate<'a> {
#[locale] #[locale]
locale: Locale<'a>, locale: Locale<'a>,
shots: Vec<ShotDetails>, shots: Vec<ShotDetails>,
lang: SupportedLanguage,
} }
#[derive(Template, TemplateUrl)] #[derive(Template, TemplateUrl)]
@ -244,14 +246,14 @@ async fn main() {
&SupportedLanguage::English.lookup(GameListTemplate::URL_KEY), &SupportedLanguage::English.lookup(GameListTemplate::URL_KEY),
get(games_for_division_html), get(games_for_division_html),
) )
//.route( .route(
// &SupportedLanguage::English.lookup(GameScorePageTemplate::URL_KEY), &SupportedLanguage::English.lookup(GameScorePageTemplate::URL_KEY),
// get(score_for_game_html), get(score_for_game_html),
//) )
//.route( .route(
// &SupportedLanguage::French.lookup(GameScorePageTemplate::URL_KEY), &SupportedLanguage::French.lookup(GameScorePageTemplate::URL_KEY),
// get(score_for_game_html), get(score_for_game_html),
//) )
//.route("/:lang/player/:name/", get(player_from_name)) //.route("/:lang/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));
@ -280,7 +282,7 @@ async fn player_from_name(
let player = Player::from_name_case_insensitive(&server_config.db_pool, name.clone()) let player = Player::from_name_case_insensitive(&server_config.db_pool, name.clone())
.await .await
.unwrap(); .unwrap();
let latest_league = Player::latest_league(&server_config.db_pool, player.id) let latest_league = Player::latest_league(&server_config.db_pool, player.id, lang.into())
.await .await
.unwrap() .unwrap()
.unwrap(); .unwrap();
@ -333,7 +335,7 @@ async fn league_html(
State(server_config): State<ServerState>, State(server_config): State<ServerState>,
Path(lang): Path<SupportedLanguage>, Path(lang): Path<SupportedLanguage>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let leagues = League::all(&*server_config.db_pool, lang).await.unwrap(); let leagues = League::all(&*server_config.db_pool, lang.into()).await.unwrap();
let leagues_template = LeagueListTemplate { let leagues_template = LeagueListTemplate {
lang_links: other_lang_urls!(lang, LeagueListTemplate), lang_links: other_lang_urls!(lang, LeagueListTemplate),
locale: lang.into(), locale: lang.into(),
@ -351,7 +353,7 @@ async fn divisions_for_league_html(
.await .await
.unwrap() .unwrap()
.unwrap(); .unwrap();
let divisions = Division::by_league(&*server_config.db_pool, league_id, lang) let divisions = Division::by_league(&*server_config.db_pool, league_id, lang.into())
.await .await
.unwrap(); .unwrap();
let html = DivisionListTemplate { let html = DivisionListTemplate {
@ -369,14 +371,14 @@ async fn games_for_division_html(
State(server_config): State<ServerState>, State(server_config): State<ServerState>,
Path((lang, division_id)): Path<(SupportedLanguage, i32)>, Path((lang, division_id)): Path<(SupportedLanguage, i32)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let division = Division::get(&*server_config.db_pool, division_id, lang) let division = Division::get(&*server_config.db_pool, division_id, lang.into())
.await .await
.unwrap() .unwrap()
.unwrap(); .unwrap();
let games = Game::by_division(&*server_config.db_pool, division.id, lang) let games = Game::by_division(&*server_config.db_pool, division.id, lang.into())
.await .await
.unwrap(); .unwrap();
let iihf_stats = division.iihf_stats(&*server_config.db_pool, lang).await.unwrap(); let iihf_stats = division.iihf_stats(&*server_config.db_pool, lang.into()).await.unwrap();
let games_template = GameListTemplate { let games_template = GameListTemplate {
locale: lang.into(), locale: lang.into(),
lang_links: other_lang_urls!(lang, GameListTemplate, "id" => division_id), lang_links: other_lang_urls!(lang, GameListTemplate, "id" => division_id),
@ -390,22 +392,19 @@ async fn games_for_division_html(
}; };
(StatusCode::OK, games_template) (StatusCode::OK, games_template)
} }
/*
async fn score_for_game_html( async fn score_for_game_html(
State(server_config): State<ServerState>, State(server_config): State<ServerState>,
Path((lang, game_id)): Path<(SupportedLanguage, i32)>, Path((lang, game_id)): Path<(SupportedLanguage, i32)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id = $1;") let game = Game::get(&*server_config.db_pool, game_id, lang.into())
.bind(game_id)
.fetch_one(&*server_config.db_pool)
.await .await
.unwrap(); .unwrap().unwrap();
let division = Division::get(&*server_config.db_pool, game.division, lang) let division = Division::get(&*server_config.db_pool, game.division, lang.into())
.await .await
.unwrap() .unwrap()
.unwrap(); .unwrap();
let pbp = game.play_by_play(&server_config.db_pool).await.unwrap(); let pbp = game.play_by_play(&server_config.db_pool, lang.into()).await.unwrap();
let score = game.score(&server_config.db_pool).await.unwrap(); let score = game.score(&server_config.db_pool, lang.into()).await.unwrap();
let score_html = TeamGameStatsTemplate { let score_html = TeamGameStatsTemplate {
locale: lang.into(), locale: lang.into(),
teams: score, teams: score,
@ -415,14 +414,16 @@ async fn score_for_game_html(
locale: lang.into(), locale: lang.into(),
players: goal_details, players: goal_details,
}; };
let box_score = game.goals(&server_config.db_pool).await.unwrap(); let box_score = game.goals(&server_config.db_pool, lang.into()).await.unwrap();
let box_score_html = BoxScoreTemplate { let box_score_html = BoxScoreTemplate {
locale: lang.into(), locale: lang.into(),
goals: box_score, goals: box_score,
lang,
}; };
let pbp_html = ShotsTableTemplate { let pbp_html = ShotsTableTemplate {
locale: lang.into(), locale: lang.into(),
shots: pbp, shots: pbp,
lang
}; };
let game_template = GameScorePageTemplate { let game_template = GameScorePageTemplate {
locale: lang.into(), locale: lang.into(),
@ -437,7 +438,6 @@ async fn score_for_game_html(
}; };
(StatusCode::OK, game_template) (StatusCode::OK, game_template)
} }
*/
/* /*
macro_rules! insert { macro_rules! insert {

@ -8,7 +8,7 @@ pub trait TableName {
} }
pub trait NameTableName { pub trait NameTableName {
const NAME_TABLE_NAME: &'static str; const NAME_TABLE_NAME: &'static str;
const NAME_TABLE_FK_NAME: &'static str; const NAME_TABLE_FK_NAME: &'static str;
} }
macro_rules! impl_table_name { macro_rules! impl_table_name {
($ty:ident, $tname:literal) => { ($ty:ident, $tname:literal) => {
@ -148,7 +148,7 @@ pub struct League {
pub name: Option<String>, pub name: Option<String>,
} }
//impl_localized_get!(League, league_name); //impl_localized_get!(League, league_name);
impl_localized_all!(League); //impl_localized_all!(League);
/* /*
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)] #[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)]
#[ormx(table_name = "leagues", table = League, id = "id")] #[ormx(table_name = "leagues", table = League, id = "id")]
@ -157,18 +157,18 @@ pub struct NewLeague {
} }
*/ */
#[derive(FromRow, Serialize, Deserialize, Debug)] #[derive(FromRow, Serialize, Deserialize, Debug, NameTableName)]
//#[ormx(table = "divisions", id = id, insertable, deletable)] #[table_names(table_name = "divisions", name_func = "division_name", name_table_name = "division_names", name_table_name_fk = "division")]
pub struct Division { pub struct Division {
//#[ormx(default)] //#[ormx(default)]
pub id: i32, pub id: i32,
//#[ormx(get_many(i32))] #[table_names(get_many)]
pub league: i32, pub league: i32,
pub name: Option<String>, pub name: Option<String>,
} }
impl_localized_get!(Division, division_name); //impl_localized_get!(Division, division_name);
impl_localized_get_by_many!(Division, league); //impl_localized_get_by_many!(Division, league);
impl_localized_all!(Division); //impl_localized_all!(Division);
#[derive(FromRow, Serialize, Deserialize, Debug)] #[derive(FromRow, Serialize, Deserialize, Debug)]
//#[ormx(table_name = "divisions", table = Division, id = "id")] //#[ormx(table_name = "divisions", table = Division, id = "id")]
@ -176,8 +176,9 @@ pub struct NewDivision {
pub league: i32, pub league: i32,
} }
#[derive(FromRow, Serialize, Deserialize, Debug)] #[derive(FromRow, Serialize, Deserialize, Debug, NameTableName)]
//#[ormx(table = "teams", id = id, insertable, deletable)] //#[ormx(table = "teams", id = id, insertable, deletable)]
#[table_names(table_name = "teams", name_func = "team_name", name_table_name = "team_names", name_table_name_fk = "team")]
pub struct Team { pub struct Team {
//#[ormx(default)] //#[ormx(default)]
pub id: i32, pub id: i32,
@ -185,7 +186,6 @@ pub struct Team {
pub image: Option<String>, pub image: Option<String>,
pub name: Option<String>, pub name: Option<String>,
} }
impl_localized_get!(Team, team_name);
/* /*
#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)] #[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)]
@ -209,10 +209,11 @@ pub struct Player {
impl Player { impl Player {
pub async fn from_name_case_insensitive(pool: &sqlx::PgPool, name: String) -> Option<Player> { pub async fn from_name_case_insensitive(pool: &sqlx::PgPool, name: String) -> Option<Player> {
sqlx::query_as::<_, Player>( sqlx::query_as!(
"SELECT * FROM players WHERE REPLACE(UPPER(name), ' ', '-') LIKE UPPER($1);", Player,
"SELECT * FROM players WHERE REPLACE(UPPER(last_name), ' ', '-') LIKE UPPER($1);",
name
) )
.bind(name)
.fetch_optional(pool) .fetch_optional(pool)
.await .await
.unwrap() .unwrap()
@ -258,19 +259,21 @@ pub struct GamePlayer {
pub game: i32, pub game: i32,
} }
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug, NameTableName)]
//#[ormx(table = "games", id = id, insertable, deletable)] #[table_names(table_name = "games", name_func = "game_name", name_table_name = "game_names", name_table_name_fk = "game")]
pub struct Game { pub struct Game {
//#[ormx(default)] //#[ormx(default)]
pub id: i32, pub id: i32,
//#[ormx(get_many(i32))] #[table_names(get_many)]
pub division: i32, pub division: i32,
pub team_home: i32, pub team_home: i32,
pub team_away: i32, pub team_away: i32,
pub name: Option<String>, pub name: Option<String>,
pub start_at: DateTime<Utc>,
pub end_at: DateTime<Utc>,
} }
impl_localized_get!(Game, game_name); //impl_localized_get!(Game, game_name);
impl_localized_get_by_many!(Game, division); //impl_localized_get_by_many!(Game, division);
#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)] #[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)]
#[ormx(table = "periods", id = id, insertable, deletable)] #[ormx(table = "periods", id = id, insertable, deletable)]
@ -281,20 +284,6 @@ pub struct Period {
pub game: i32, pub game: i32,
} }
impl_table_name!(GamePlayer, "game_players");
impl_table_name!(Player, "players");
impl_table_name!(League, "leagues");
//impl_name_table_name!(League, "league_names", "league");
impl_table_name!(Division, "divisions");
impl_name_table_name!(Division, "division_names", "division");
impl_table_name!(Team, "teams");
impl_name_table_name!(Team, "team_names", "team");
impl_table_name!(Shot, "shots");
impl_table_name!(Game, "games");
impl_name_table_name!(Game, "game_names", "game");
impl_table_name!(Period, "periods");
impl_table_name!(Language, "supported_languages");
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::languages::SupportedLanguage; use crate::languages::SupportedLanguage;
@ -328,7 +317,7 @@ mod tests {
fn test_get_player_from_name() { fn test_get_player_from_name() {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let player = Player::from_name_case_insensitive(&pool, "tait-hoyem".to_string()).await; let player = Player::from_name_case_insensitive(&pool, "hoyem".to_string()).await;
assert!(player.is_some()); assert!(player.is_some());
let player = player.unwrap(); let player = player.unwrap();
assert_eq!(player.first_names, "Tait"); assert_eq!(player.first_names, "Tait");
@ -356,14 +345,7 @@ mod tests {
fn $func_name() { fn $func_name() {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let results = sqlx::query_as::<_, $ret_type>(&format!( let results = $ret_type::all(&pool, SupportedLanguage::English.into()).await.unwrap();
"SELECT * FROM {};",
<$ret_type as TableName>::TABLE_NAME
))
.fetch_all(&pool)
.await
.unwrap();
// check that there is at least one result item
assert!( assert!(
results.len() > 0, results.len() > 0,
"There must be at least one result in the table." "There must be at least one result in the table."
@ -372,12 +354,12 @@ mod tests {
} }
}; };
} }
generate_select_test!(GamePlayer, selec_game_player); //generate_select_test!(GamePlayer, selec_game_player);
generate_select_test!(Player, select_player); // generate_select_test!(Player, select_player);
generate_select_test!(League, select_league); generate_select_test!(League, select_league);
generate_select_test!(Division, select_division); generate_select_test!(Division, select_division);
generate_select_test!(Team, select_team); generate_select_test!(Team, select_team);
generate_select_test!(Shot, select_shot); //generate_select_test!(Shot, select_shot);
generate_select_test!(Game, select_game); generate_select_test!(Game, select_game);
generate_select_test!(Language, select_lang); //generate_select_test!(Language, select_lang);
} }

@ -15,7 +15,7 @@ pub struct TeamStats {
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct IihfStats { pub struct IihfStats {
pub team_name: String, pub team_name: Option<String>,
pub team_id: i32, pub team_id: i32,
pub reg_wins: i32, pub reg_wins: i32,
pub reg_losses: i32, pub reg_losses: i32,
@ -26,7 +26,7 @@ pub struct IihfStats {
} }
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct IihfStatsI64 { pub struct IihfStatsI64 {
pub team_name: String, pub team_name: Option<String>,
pub team_id: i32, pub team_id: i32,
pub reg_wins: i64, pub reg_wins: i64,
pub reg_losses: i64, pub reg_losses: i64,
@ -59,7 +59,8 @@ pub struct IihfPoints {
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct Notification { pub struct Notification {
pub scorer_name: String, pub scorer_first_names: String,
pub scorer_last_name: String,
pub scorer_number: i32, pub scorer_number: i32,
pub position: String, pub position: String,
pub scorer_team_name: String, pub scorer_team_name: String,
@ -69,7 +70,8 @@ pub struct Notification {
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct PlayerStats { pub struct PlayerStats {
pub name: String, pub first_names: String,
pub last_name: String,
pub goals: i64, pub goals: i64,
pub assists: i64, pub assists: i64,
pub points: i64, pub points: i64,
@ -81,7 +83,8 @@ SELECT
COUNT(shots.id) AS points, COUNT(shots.id) AS points,
COUNT(CASE WHEN shots.shooter = game_players.id THEN shots.id END) AS goals, COUNT(CASE WHEN shots.shooter = game_players.id THEN shots.id END) AS goals,
COUNT(CASE WHEN shots.assistant = game_players.id OR shots.assistant_second = game_players.id THEN shots.id END) AS assists, COUNT(CASE WHEN shots.assistant = game_players.id OR shots.assistant_second = game_players.id THEN shots.id END) AS assists,
players.name players.first_names,
players.last_name
FROM game_players FROM game_players
JOIN players JOIN players
ON game_players.player = players.id ON game_players.player = players.id
@ -93,7 +96,8 @@ LEFT JOIN shots
WHERE game_players.game=$1 WHERE game_players.game=$1
GROUP BY GROUP BY
game_players.id, game_players.id,
players.name players.last_name,
players.first_names
HAVING COUNT(shots.id) > 0 HAVING COUNT(shots.id) > 0
ORDER BY ORDER BY
points DESC, points DESC,
@ -104,20 +108,23 @@ ORDER BY
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
pub async fn game_goals(pool: &PgPool, game_id: i32) -> Result<Vec<GoalDetails>, sqlx::Error> { pub async fn game_goals(pool: &PgPool, game_id: i32, lang: i32) -> Result<Vec<GoalDetails>, sqlx::Error> {
sqlx::query_as::<_, GoalDetails>( sqlx::query_as::<_, GoalDetails>(
r#" r#"
SELECT SELECT
shots.shooter AS player_id, shots.shooter AS player_id,
shots.assistant AS first_assist_id, shots.assistant AS first_assist_id,
shots.assistant_second AS second_assist_id, shots.assistant_second AS second_assist_id,
players.name AS player_name, players.first_names AS player_first_names,
p_assist.name AS first_assist_name, players.last_name AS player_last_name,
p_assist_second.name AS second_assist_name, p_assist.first_names AS first_assist_first_names,
p_assist.last_name AS first_assist_last_name,
p_assist_second.first_names AS second_assist_first_names,
p_assist_second.last_name AS second_assist_last_name,
game_players.player_number AS player_number, game_players.player_number AS player_number,
gp_assist.player_number AS first_assist_number, gp_assist.player_number AS first_assist_number,
gp_assist_second.player_number AS second_assist_number, gp_assist_second.player_number AS second_assist_number,
teams.name AS team_name, team_name(teams.id, $1) AS team_name,
teams.id AS team_id, teams.id AS team_id,
shots.period_time AS time_remaining, shots.period_time AS time_remaining,
period_types.id AS period_id, period_types.id AS period_id,
@ -141,14 +148,15 @@ pub async fn game_goals(pool: &PgPool, game_id: i32) -> Result<Vec<GoalDetails>,
"#, "#,
) )
.bind(game_id) .bind(game_id)
.bind(lang)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
pub async fn game_iihf_stats(pool: &PgPool, game_id: i32) -> Result<Vec<IihfStats>, sqlx::Error> { pub async fn game_iihf_stats(pool: &PgPool, game_id: i32, lang: i32) -> Result<Vec<IihfStats>, sqlx::Error> {
let query = r#" let query = r#"
SELECT SELECT
teams.id AS team_id, teams.id AS team_id,
teams.name AS team_name, team_name(teams.id, $2) AS team_name,
reg_win(games.id, teams.id) AS reg_wins, reg_win(games.id, teams.id) AS reg_wins,
reg_loss(games.id, teams.id) AS reg_losses, reg_loss(games.id, teams.id) AS reg_losses,
ot_win(games.id, teams.id) AS ot_wins, ot_win(games.id, teams.id) AS ot_wins,
@ -169,6 +177,7 @@ pub async fn game_iihf_stats(pool: &PgPool, game_id: i32) -> Result<Vec<IihfStat
"#; "#;
sqlx::query_as::<_, IihfStats>(query) sqlx::query_as::<_, IihfStats>(query)
.bind(game_id) .bind(game_id)
.bind(lang)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
@ -176,11 +185,11 @@ pub async fn game_iihf_stats(pool: &PgPool, game_id: i32) -> Result<Vec<IihfStat
/// NOTE: The algorithm used here requires that a 4th period is the "overtime"; /// 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. /// it does not check if there was only two periods, followed by an overtime.
/// This should be sufficient for most. /// This should be sufficient for most.
pub async fn game_iihf_points(pool: &PgPool, game_id: i32) -> Result<Vec<IihfPoints>, sqlx::Error> { pub async fn game_iihf_points(pool: &PgPool, game_id: i32, lang: i32) -> Result<Vec<IihfPoints>, sqlx::Error> {
let query = r#" let query = r#"
SELECT SELECT
iihf_points(games.id, teams.id) AS points, iihf_points(games.id, teams.id) AS points,
teams.name AS team_name, team_name(teams.id, $1) AS team_name,
teams.id AS team_id teams.id AS team_id
FROM games FROM games
JOIN teams JOIN teams
@ -191,16 +200,17 @@ pub async fn game_iihf_points(pool: &PgPool, game_id: i32) -> Result<Vec<IihfPoi
"#; "#;
sqlx::query_as::<_, IihfPoints>(query) sqlx::query_as::<_, IihfPoints>(query)
.bind(game_id) .bind(game_id)
.bind(lang)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
/// Returns the number of shots and goals for each team in the game. /// Returns the number of shots and goals for each team in the game.
pub async fn game_score(pool: &PgPool, game_id: i32) -> Result<Vec<TeamStats>, sqlx::Error> { pub async fn game_score(pool: &PgPool, game_id: i32, lang: i32) -> Result<Vec<TeamStats>, sqlx::Error> {
let query = r#" let query = r#"
SELECT SELECT
COUNT(CASE WHEN shots.goal = true THEN shots.id END) AS goals, COUNT(CASE WHEN shots.goal = true THEN shots.id END) AS goals,
COUNT(shots.id) AS shots, COUNT(shots.id) AS shots,
teams.name AS name team_name(teams.id, $2) AS name
FROM games FROM games
JOIN periods ON periods.game=games.id JOIN periods ON periods.game=games.id
JOIN shots ON shots.period=periods.id JOIN shots ON shots.period=periods.id
@ -211,12 +221,14 @@ pub async fn game_score(pool: &PgPool, game_id: i32) -> Result<Vec<TeamStats>, s
"#; "#;
sqlx::query_as::<_, TeamStats>(query) sqlx::query_as::<_, TeamStats>(query)
.bind(game_id) .bind(game_id)
.bind(lang)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
pub async fn game_play_by_play( pub async fn game_play_by_play(
pool: &PgPool, pool: &PgPool,
game_id: i32, game_id: i32,
lang: i32,
) -> Result<Vec<ShotDetails>, sqlx::Error> { ) -> Result<Vec<ShotDetails>, sqlx::Error> {
sqlx::query_as::<_, ShotDetails>( sqlx::query_as::<_, ShotDetails>(
r#" r#"
@ -225,13 +237,16 @@ SELECT
shots.assistant AS first_assist_id, shots.assistant AS first_assist_id,
shots.assistant_second AS second_assist_id, shots.assistant_second AS second_assist_id,
shots.goal AS is_goal, shots.goal AS is_goal,
players.name AS player_name, players.first_names AS player_first_names,
p_assistant.name AS first_assist_name, players.last_name AS player_last_name,
p_assistant_second.name AS second_assist_name, p_assistant.first_names AS first_assist_first_names,
p_assistant.last_name AS first_assist_last_name,
p_assistant_second.first_names AS second_assist_first_names,
p_assistant_second.last_name AS second_assist_last_name,
game_players.player_number AS player_number, game_players.player_number AS player_number,
gp_assistant.player_number AS first_assist_number, gp_assistant.player_number AS first_assist_number,
gp_assistant_second.player_number AS second_assist_number, gp_assistant_second.player_number AS second_assist_number,
teams.name AS team_name, team_name(teams.id, $2) AS team_name,
teams.id AS team_id, teams.id AS team_id,
shots.period_time AS time_remaining, shots.period_time AS time_remaining,
period_types.id AS period_id, period_types.id AS period_id,
@ -253,79 +268,93 @@ ORDER BY
"#, "#,
) )
.bind(game_id) .bind(game_id)
.bind(lang)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
impl Game { impl Game {
pub async fn score(&self, pool: &PgPool) -> Result<Vec<TeamStats>, sqlx::Error> { pub async fn score(&self, pool: &PgPool, lang: i32) -> Result<Vec<TeamStats>, sqlx::Error> {
game_score(pool, self.id).await game_score(pool, self.id, lang).await
} }
pub async fn box_score(&self, pool: &PgPool) -> Result<Vec<PlayerStats>, sqlx::Error> { pub async fn box_score(&self, pool: &PgPool) -> Result<Vec<PlayerStats>, sqlx::Error> {
game_box_score(pool, self.id).await game_box_score(pool, self.id).await
} }
pub async fn iihf_points(&self, pool: &PgPool) -> Result<Vec<IihfPoints>, sqlx::Error> { pub async fn iihf_points(&self, pool: &PgPool, lang: i32) -> Result<Vec<IihfPoints>, sqlx::Error> {
game_iihf_points(pool, self.id).await game_iihf_points(pool, self.id, lang).await
} }
pub async fn iihf_stats(&self, pool: &PgPool) -> Result<Vec<IihfStats>, sqlx::Error> { pub async fn iihf_stats(&self, pool: &PgPool, lang: i32) -> Result<Vec<IihfStats>, sqlx::Error> {
game_iihf_stats(pool, self.id).await game_iihf_stats(pool, self.id, lang).await
} }
pub async fn goals(&self, pool: &PgPool) -> Result<Vec<GoalDetails>, sqlx::Error> { pub async fn goals(&self, pool: &PgPool, lang: i32) -> Result<Vec<GoalDetails>, sqlx::Error> {
game_goals(pool, self.id).await game_goals(pool, self.id, lang).await
} }
pub async fn play_by_play(&self, pool: &PgPool) -> Result<Vec<ShotDetails>, sqlx::Error> { pub async fn play_by_play(&self, pool: &PgPool, lang: i32) -> Result<Vec<ShotDetails>, sqlx::Error> {
game_play_by_play(pool, self.id).await game_play_by_play(pool, self.id, lang).await
} }
} }
pub async fn division_iihf_stats( pub async fn division_iihf_stats(
pool: &PgPool, pool: &PgPool,
division_id: i32, division_id: i32,
lang: SupportedLanguage, lang: i32,
) -> Result<Vec<IihfStatsI64>, sqlx::Error> { ) -> Result<Vec<IihfStatsI64>, sqlx::Error> {
sqlx::query_as::<_, IihfStatsI64>( sqlx::query_as!(
IihfStatsI64,
r#" r#"
WITH team_name AS (
SELECT
teams.id AS team_id,
-- max will get the first matching string; technically it will always get the string that that has the maximum value based on the locale, ignoring nulls.
COALESCE(
MAX(localized_name.name),
MAX(default_name.name),
MAX(any_name.name)
) AS team_name,
-- this is to get the language id of the team name; although not strictly necessary, since we use MIN(...), then ORDER BY it later on, we prioritize languages that have been added earlier, making this table deterministic
COALESCE(
MIN(localized_name.language),
MIN(default_name.language),
MIN(any_name.language)
) AS name_language
FROM teams
LEFT JOIN team_names localized_name ON localized_name.team = teams.id AND localized_name.language = $2
LEFT JOIN team_names default_name ON default_name.team = teams.id AND default_name.language = 1
LEFT JOIN team_names any_name ON any_name.team = teams.id
GROUP BY teams.id
ORDER BY name_language
)
SELECT SELECT
SUM(reg_win(games.id, teams.id)) AS reg_wins, SUM(points) AS "points!",
SUM(reg_loss(games.id, teams.id)) AS reg_losses, SUM(reg_wins) AS "reg_wins!",
SUM(ot_win(games.id, teams.id)) AS ot_wins, SUM(reg_losses) AS "reg_losses!",
SUM(ot_loss(games.id, teams.id)) AS ot_losses, SUM(ot_wins) AS "ot_wins!",
SUM(tie(games.id, teams.id)) AS ties, SUM(ot_losses) AS "ot_losses!",
SUM(iihf_points(games.id, teams.id)) AS points, SUM(ties) AS "ties!",
teams.id AS team_id, team_name(team_id, $2) AS team_name,
team_name.team_name team_id AS "team_id!"
FROM FROM team_points_view
games WHERE division_id=$1
JOIN teams ON teams.id=games.team_home OR teams.id=games.team_away GROUP BY team_id;
JOIN team_name ON team_name.team_id=teams.id --WITH team_name AS (
WHERE games.division=$1 -- SELECT
GROUP BY -- teams.id AS team_id,
teams.id, -- -- max will get the first matching string; technically it will always get the string that that has the maximum value based on the locale, ignoring nulls.
team_name.team_name -- COALESCE(
ORDER BY -- MAX(localized_name.name),
points DESC; -- MAX(default_name.name),
-- MAX(any_name.name)
-- ) AS team_name,
-- -- this is to get the language id of the team name; although not strictly necessary, since we use MIN(...), then ORDER BY it later on, we prioritize languages that have been added earlier, making this table deterministic
-- COALESCE(
-- MIN(localized_name.language),
-- MIN(default_name.language),
-- MIN(any_name.language)
-- ) AS name_language
-- FROM teams
-- LEFT JOIN team_names localized_name ON localized_name.team = teams.id AND localized_name.language = $2
-- LEFT JOIN team_names default_name ON default_name.team = teams.id AND default_name.language = 1
-- LEFT JOIN team_names any_name ON any_name.team = teams.id
-- GROUP BY teams.id
-- ORDER BY name_language
--)
--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,
-- team_name.team_name
--FROM
-- games
--JOIN teams ON teams.id=games.team_home OR teams.id=games.team_away
--JOIN team_name ON team_name.team_id=teams.id
--WHERE games.division=$1
--GROUP BY
-- teams.id,
-- team_name.team_name
--ORDER BY
-- points DESC;
--SELECT DISTINCT ON (teams.id) --SELECT DISTINCT ON (teams.id)
-- SUM(reg_win(games.id, teams.id)) AS reg_wins, -- SUM(reg_win(games.id, teams.id)) AS reg_wins,
@ -361,23 +390,22 @@ ORDER BY
-- teams.id, -- teams.id,
-- points DESC; -- points DESC;
"#, "#,
division_id, lang
) )
.bind(division_id)
.bind(lang)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
impl Division { impl Division {
pub async fn iihf_stats(&self, pool: &PgPool, lang: SupportedLanguage) -> Result<Vec<IihfStatsI64>, sqlx::Error> { pub async fn iihf_stats(&self, pool: &PgPool, lang: i32) -> Result<Vec<IihfStatsI64>, sqlx::Error> {
division_iihf_stats(pool, self.id, lang).await division_iihf_stats(pool, self.id, lang).await
} }
} }
impl Player { impl Player {
pub async fn latest_league(pool: &PgPool, id: i32) -> Result<Option<League>, sqlx::Error> { pub async fn latest_league(pool: &PgPool, id: i32, lang: i32) -> Result<Option<League>, sqlx::Error> {
let query = r#" let query = r#"
SELECT leagues.* SELECT leagues.*,team_name(teams.id, $2) AS name
FROM players FROM players
JOIN game_players ON game_players.player=players.id JOIN game_players ON game_players.player=players.id
JOIN games ON games.id=game_players.game JOIN games ON games.id=game_players.game
@ -390,22 +418,26 @@ impl Player {
"#; "#;
sqlx::query_as::<_, League>(query) sqlx::query_as::<_, League>(query)
.bind(id) .bind(id)
.bind(lang)
.fetch_optional(pool) .fetch_optional(pool)
.await .await
} }
pub async fn latest_stats(pool: &PgPool, id: i32) -> Result<Vec<GoalDetails>, sqlx::Error> { pub async fn latest_stats(pool: &PgPool, id: i32, lang: i32) -> Result<Vec<GoalDetails>, sqlx::Error> {
let query = r#" let query = r#"
SELECT SELECT
players.id AS player_id, players.id AS player_id,
p_assist.id AS first_assist_id, p_assist.id AS first_assist_id,
p_assist_second.id AS second_assist_id, p_assist_second.id AS second_assist_id,
players.name AS player_name, players.first_names AS player_first_names,
p_assist.name AS first_assist_name, players.last_name AS player_last_name,
p_assist_second.name AS second_assist_name, p_assist.first_names AS first_assist_first_names,
p_assist.last_name AS first_assist_last_name,
p_assist_second.first_names AS second_assist_first_names,
p_assist_second.last_name AS second_assist_last_name,
game_players.player_number AS player_number, game_players.player_number AS player_number,
gp_assist.player_number AS first_assist_number, gp_assist.player_number AS first_assist_number,
gp_assist_second.player_number AS second_assist_number, gp_assist_second.player_number AS second_assist_number,
teams.name AS team_name, team_name(teams.id, $2) AS team_name,
teams.id AS team_id, teams.id AS team_id,
shots.period_time AS time_remaining, shots.period_time AS time_remaining,
period_types.id AS period_id, period_types.id AS period_id,
@ -429,6 +461,7 @@ impl Player {
"#; "#;
sqlx::query_as::<_, GoalDetails>(&query) sqlx::query_as::<_, GoalDetails>(&query)
.bind(id) .bind(id)
.bind(lang)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
@ -438,7 +471,8 @@ impl Player {
COUNT(goals) AS goals, COUNT(goals) AS goals,
COUNT(assists) AS assists, COUNT(assists) AS assists,
COUNT(points) AS points, COUNT(points) AS points,
players.name AS name players.first_names AS first_names,
players.last_name AS last_name
FROM players FROM players
JOIN game_players ON game_players.player=players.id JOIN game_players ON game_players.player=players.id
LEFT JOIN shots points LEFT JOIN shots points
@ -468,7 +502,8 @@ SELECT
COUNT(shots.id) AS points, COUNT(shots.id) AS points,
COUNT(CASE WHEN shots.shooter = game_players.id THEN shots.id END) AS goals, COUNT(CASE WHEN shots.shooter = game_players.id THEN shots.id END) AS goals,
COUNT(CASE WHEN shots.assistant = game_players.id OR shots.assistant_second = game_players.id THEN shots.id END) AS assists, COUNT(CASE WHEN shots.assistant = game_players.id OR shots.assistant_second = game_players.id THEN shots.id END) AS assists,
players.name players.first_names AS first_names,
players.last_name AS last_name
FROM game_players FROM game_players
JOIN players ON game_players.player = players.id JOIN players ON game_players.player = players.id
LEFT JOIN shots LEFT JOIN shots
@ -478,7 +513,8 @@ LEFT JOIN shots
OR shots.assistant_second=game_players.id) OR shots.assistant_second=game_players.id)
GROUP BY GROUP BY
game_players.id, game_players.id,
players.name players.first_names,
players.last_name
ORDER BY ORDER BY
points DESC, points DESC,
goals DESC; goals DESC;
@ -499,7 +535,8 @@ impl League {
COUNT(goals.id) AS goals, COUNT(goals.id) AS goals,
COUNT(assists.id) AS assists, COUNT(assists.id) AS assists,
COUNT(points.id) AS points, COUNT(points.id) AS points,
players.name AS name players.first_names AS first_names,
players.last_name AS last_name
FROM players FROM players
JOIN game_players ON game_players.player=players.id JOIN game_players ON game_players.player=players.id
LEFT JOIN shots goals LEFT JOIN shots goals
@ -532,17 +569,20 @@ impl League {
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct GoalDetails { pub struct GoalDetails {
pub player_id: i32, pub player_id: i32,
pub player_name: String, pub player_first_names: String,
pub player_last_name: String,
pub player_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,
pub period_id: i32, pub period_id: i32,
pub period_short_name: String, pub period_short_name: String,
pub first_assist_name: Option<String>, pub first_assist_first_names: Option<String>,
pub first_assist_last_name: Option<String>,
pub first_assist_number: Option<i32>, pub first_assist_number: Option<i32>,
pub first_assist_id: Option<i32>, pub first_assist_id: Option<i32>,
pub second_assist_name: Option<String>, pub second_assist_first_names: Option<String>,
pub second_assist_last_name: Option<String>,
pub second_assist_id: Option<i32>, pub second_assist_id: Option<i32>,
pub second_assist_number: Option<i32>, pub second_assist_number: Option<i32>,
} }
@ -550,17 +590,20 @@ pub struct GoalDetails {
#[derive(FromRow, Deserialize, Serialize, Debug)] #[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct ShotDetails { pub struct ShotDetails {
pub player_id: i32, pub player_id: i32,
pub player_name: String, pub player_first_names: String,
pub player_last_name: String,
pub player_number: i32, pub player_number: i32,
pub team_name: String, pub team_name: String,
pub team_id: i32, pub team_id: i32,
pub is_goal: bool, pub is_goal: bool,
pub time_remaining: i32, pub time_remaining: i32,
pub period_short_name: String, pub period_short_name: String,
pub first_assist_name: Option<String>, pub first_assist_first_names: Option<String>,
pub first_assist_last_name: Option<String>,
pub first_assist_number: Option<i32>, pub first_assist_number: Option<i32>,
pub first_assist_id: Option<i32>, pub first_assist_id: Option<i32>,
pub second_assist_name: Option<String>, pub second_assist_first_names: Option<String>,
pub second_assist_last_name: Option<String>,
pub second_assist_id: Option<i32>, pub second_assist_id: Option<i32>,
pub second_assist_number: Option<i32>, pub second_assist_number: Option<i32>,
} }
@ -580,7 +623,7 @@ mod tests {
fn check_play_by_play() { fn check_play_by_play() {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let pbp = game_play_by_play(&pool, 3).await.unwrap(); let pbp = game_play_by_play(&pool, 3, SupportedLanguage::English.into()).await.unwrap();
}) })
} }
@ -589,7 +632,7 @@ mod tests {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let player = Player::get(&pool, 2).await.unwrap(); let player = Player::get(&pool, 2).await.unwrap();
let latest = Player::latest_stats(&pool, player.id).await.unwrap(); let latest = Player::latest_stats(&pool, player.id, SupportedLanguage::English.into()).await.unwrap();
}) })
} }
@ -597,12 +640,12 @@ mod tests {
fn check_league_player_stats() { fn check_league_player_stats() {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let league = League::get(&pool, 1, SupportedLanguage::English).await.unwrap().unwrap(); let league = League::get(&pool, 1, SupportedLanguage::English.into()).await.unwrap().unwrap();
let player = Player::get(&pool, 2).await.unwrap(); let player = Player::get(&pool, 2).await.unwrap();
let stats = League::player_stats(&pool, player.id, league.id) let stats = League::player_stats(&pool, player.id, league.id)
.await .await
.unwrap(); .unwrap();
assert_eq!(stats.name, "Hillary Scanlon"); assert_eq!(stats.last_name, "Scanlon");
}) })
} }
@ -611,7 +654,7 @@ mod tests {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; 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) let league = Player::latest_league(&pool, player.id, SupportedLanguage::English.into())
.await .await
.unwrap() .unwrap()
.unwrap(); .unwrap();
@ -623,7 +666,7 @@ mod tests {
fn check_score_details_from_game() { fn check_score_details_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 scores = game_goals(&pool, 3).await.unwrap(); let scores = game_goals(&pool, 3, SupportedLanguage::English.into()).await.unwrap();
println!("{scores:?}"); println!("{scores:?}");
}) })
} }
@ -635,7 +678,7 @@ mod tests {
let scores = game_box_score(&pool, 4).await.unwrap(); let scores = game_box_score(&pool, 4).await.unwrap();
println!("{scores:?}"); println!("{scores:?}");
let second_top_scorer = scores.get(1).unwrap(); let second_top_scorer = scores.get(1).unwrap();
assert_eq!(second_top_scorer.name, "Allyssa Foulds"); assert_eq!(second_top_scorer.last_name, "Foulds");
assert_eq!(second_top_scorer.goals, 1, "Allysa should have 1 goal.."); assert_eq!(second_top_scorer.goals, 1, "Allysa should have 1 goal..");
assert_eq!( assert_eq!(
second_top_scorer.assists, 2, second_top_scorer.assists, 2,
@ -654,14 +697,16 @@ mod tests {
fn check_division_iihf_stats() { fn check_division_iihf_stats() {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let score = division_iihf_stats(&pool, 1, SupportedLanguage::English).await.unwrap(); let score = division_iihf_stats(&pool, 1, SupportedLanguage::English.into()).await.unwrap();
let team_1 = score.get(0).unwrap();
let team_2 = score.get(1).unwrap();
assert_eq!(score.len(), 2, "Too many teams selected."); assert_eq!(score.len(), 2, "Too many teams selected.");
assert_eq!(score.get(0).unwrap().points, 10, "Top team should have 10 points"); assert_eq!(team_1.points, 10, "Top team should have 10 points");
assert_eq!(score.get(0).unwrap().team_name, "Bullseye", "Top team should be bullseye"); assert_eq!(team_1.team_name.as_ref().unwrap(), "Bullseye", "Top team should be bullseye");
assert_eq!(score.get(0).unwrap().reg_losses, 0, "The bullseye should have no regulation losses"); assert_eq!(team_1.reg_losses, 0, "The bullseye should have no regulation losses");
assert_eq!(score.get(0).unwrap().ties, 2, "There should be two ties for the bullsye"); assert_eq!(team_1.ties, 2, "There should be two ties for the bullsye");
assert_eq!(score.get(1).unwrap().team_name, "See Cats", "The second-place team should be the see cats"); assert_eq!(team_2.team_name.as_ref().unwrap(), "See Cats", "The second-place team should be the see cats");
assert_eq!(score.get(1).unwrap().points, 4, "The second-place team should have four points"); assert_eq!(team_2.points, 4, "The second-place team should have four points");
}) })
} }
@ -669,11 +714,12 @@ mod tests {
fn check_iihf_stats() { fn check_iihf_stats() {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let score = game_iihf_stats(&pool, 4).await.unwrap(); let score = game_iihf_stats(&pool, 4, SupportedLanguage::English.into()).await.unwrap();
assert_eq!(score.get(0).unwrap().points, 2); let team_1 = score.get(0).unwrap();
assert_eq!(score.get(0).unwrap().team_name, "Bullseye"); assert_eq!(team_1.points, 2);
assert_eq!(score.get(0).unwrap().reg_losses, 0); assert_eq!(team_1.team_name.as_ref().unwrap(), "Bullseye");
assert_eq!(score.get(0).unwrap().ties, 1); assert_eq!(team_1.reg_losses, 0);
assert_eq!(team_1.ties, 1);
assert_eq!(score.get(1).unwrap().points, 2); assert_eq!(score.get(1).unwrap().points, 2);
}) })
} }
@ -682,7 +728,7 @@ mod tests {
fn check_iihf_points() { fn check_iihf_points() {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let score = game_iihf_points(&pool, 4).await.unwrap(); let score = game_iihf_points(&pool, 4, SupportedLanguage::English.into()).await.unwrap();
assert_eq!(score.get(0).unwrap().points, 2); assert_eq!(score.get(0).unwrap().points, 2);
assert_eq!(score.get(0).unwrap().team_name, "Bullseye"); assert_eq!(score.get(0).unwrap().team_name, "Bullseye");
assert_eq!(score.get(1).unwrap().points, 2); assert_eq!(score.get(1).unwrap().points, 2);
@ -693,7 +739,7 @@ mod tests {
fn check_game_score() { fn check_game_score() {
tokio_test::block_on(async move { tokio_test::block_on(async move {
let pool = db_connect().await; let pool = db_connect().await;
let score = game_score(&pool, 1).await.unwrap(); let score = game_score(&pool, 1, SupportedLanguage::English.into()).await.unwrap();
assert_eq!(score.get(0).unwrap().goals, 1); assert_eq!(score.get(0).unwrap().goals, 1);
assert_eq!(score.get(1).unwrap().goals, 1); assert_eq!(score.get(1).unwrap().goals, 1);
}) })
@ -724,8 +770,9 @@ mod tests {
let pool = db_connect().await; let pool = db_connect().await;
let query = r#" let query = r#"
SELECT SELECT
teams.name AS scorer_team_name, team_name(teams.id, $1) AS scorer_team_name,
players.name AS scorer_name, players.first_names AS scorer_first_names,
players.last_name AS scorer_last_name,
positions.name AS position, positions.name AS position,
game_players.player_number AS scorer_number, game_players.player_number AS scorer_number,
shots.period_time AS period_time_left, shots.period_time AS period_time_left,
@ -740,6 +787,7 @@ JOIN period_types ON period_types.id=periods.period_type
JOIN positions ON positions.id=game_players.position; JOIN positions ON positions.id=game_players.position;
"#; "#;
let result = sqlx::query_as::<_, Notification>(query) let result = sqlx::query_as::<_, Notification>(query)
.bind(1)
.fetch_one(&pool) .fetch_one(&pool)
.await .await
.unwrap(); .unwrap();
@ -749,7 +797,7 @@ JOIN positions ON positions.id=game_players.position;
"{0} {1} player #{3} {2} has scored! Time of the goal: {4}:{5} in the {6}", "{0} {1} player #{3} {2} has scored! Time of the goal: {4}:{5} in the {6}",
result.scorer_team_name, result.scorer_team_name,
result.position, result.position,
result.scorer_name, result.scorer_last_name,
result.scorer_number, result.scorer_number,
minutes, minutes,
seconds, seconds,

@ -1,6 +1,6 @@
{% extends "master.html" %} {% extends "master.html" %}
{% block title %}Games for {{ division.name.clone().unwrap_or("???".to_string()) }}{% endblock %} {% block title %}Games for {{ division.name|nullable }}{% endblock %}
{% block content %} {% block content %}
<h1>Division: {{ division.name|nullable }}</h1> <h1>Division: {{ division.name|nullable }}</h1>

@ -1,6 +1,6 @@
{% extends "master.html" %} {% extends "master.html" %}
{% block title %}{{ game.name.clone().unwrap() }}{% endblock %} {% block title %}{{ game.name|nullable }}{% endblock %}
{% block content %} {% block content %}
<h1>{{ localize("game-of-division", game: game.name.clone().unwrap(), division: division.name.clone().unwrap()) }}</h1> <h1>{{ localize("game-of-division", game: game.name.clone().unwrap(), division: division.name.clone().unwrap()) }}</h1>

@ -11,24 +11,16 @@
<tbody> <tbody>
{% for goal in goals %} {% for goal in goals %}
<tr> <tr>
<td>{{ goal.player_name }}</td> <td>{{ goal|goal_player_name }}</td>
<td>{{ goal.team_name }}</td> <td>{{ goal.team_name }}</td>
<td>{{ goal.player_number }}</td> <td>{{ goal.player_number }}</td>
<td>{{ goal.period_short_name }}</td> <td>{{ goal.period_short_name }}</td>
<td>{{ goal.time_remaining|seconds_as_time }}</td> <td>{{ goal.time_remaining|seconds_as_time }}</td>
<td> <td>
{% if goal.first_assist_name.is_some() %} {{ goal|goal_assist_name(lang) }}
{{ goal.first_assist_name.as_ref().unwrap() }}
{% else %}
{{ localize("unassisted") }}
{% endif %}
</td> </td>
<td> <td>
{% if goal.second_assist_name.is_some() %} {{ goal|goal_second_assist_name(lang) }}
{{ goal.second_assist_name.as_ref().unwrap() }}
{% else %}
{{ localize("not-applicable") }}
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

@ -13,7 +13,7 @@
<tbody> <tbody>
{% for team in iihf_stats %} {% for team in iihf_stats %}
<tr> <tr>
<td>{{ team.team_name }}</td> <td>{{ team.team_name|nullable }}</td>
<td>{{ team.points }}</td> <td>{{ team.points }}</td>
<td>{{ team.reg_wins }}</td> <td>{{ team.reg_wins }}</td>
<td>{{ team.reg_losses }}</td> <td>{{ team.reg_losses }}</td>

@ -10,7 +10,7 @@
<tbody> <tbody>
{% for player in players %} {% for player in players %}
<tr> <tr>
<td>{{ player.name }}</td> <td>{{ player.first_names|initials }} {{ player.last_name }}</td>
<td>{{ player.points }}</td> <td>{{ player.points }}</td>
<td>{{ player.goals }}</td> <td>{{ player.goals }}</td>
<td>{{ player.assists }}</td> <td>{{ player.assists }}</td>

@ -12,7 +12,7 @@
<tbody> <tbody>
{% for shot in shots %} {% for shot in shots %}
<tr> <tr>
<td>{{ shot.player_name }}</td> <td>{{ shot.player_first_names|initials }} {{ shot.player_last_name }}</td>
<td>{{ shot.team_name }}</td> <td>{{ shot.team_name }}</td>
<td>{{ shot.player_number }}</td> <td>{{ shot.player_number }}</td>
<td> <td>
@ -25,22 +25,10 @@
<td>{{ shot.period_short_name }}</td> <td>{{ shot.period_short_name }}</td>
<td>{{ shot.time_remaining|seconds_as_time }}</td> <td>{{ shot.time_remaining|seconds_as_time }}</td>
<td> <td>
{% if shot.is_goal %} {{ shot|shot_assist_name(lang) }}
{% if shot.first_assist_name.is_some() %}
{{ shot.first_assist_name.as_ref().unwrap() }}
{% else %}
{{ localize("unassisted") }}
{% endif %}
{% else %}
{{ localize("not-applicable") }}
{% endif %}
</td> </td>
<td> <td>
{% if shot.second_assist_name.is_some() %} {{ shot|shot_second_assist_name(lang) }}
{{ shot.second_assist_name.as_ref().unwrap() }}
{% else %}
{{ localize("not-applicable") }}
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

Loading…
Cancel
Save