diff --git a/ibihf-macros/src/lib.rs b/ibihf-macros/src/lib.rs index 63d094c..d144236 100644 --- a/ibihf-macros/src/lib.rs +++ b/ibihf-macros/src/lib.rs @@ -1,7 +1,22 @@ use darling::FromDeriveInput; use proc_macro::{self, TokenStream}; +use proc_macro2::{Span, TokenStream as TokenStream2}; 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)] #[darling(default, attributes(urls))] @@ -40,12 +55,61 @@ struct TableNameOpts { name_table_name_fk: String, } +fn get_map_filter(field: &Field) -> Option { + 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 { + 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))] pub fn derive_get(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input); let opts = TableNameOpts::from_derive_input(&input).expect("Wrong options"); 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 = fields.iter().filter_map(get_many_map_filter).collect(); + let by_many_funcs: Vec = 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, sqlx::Error> { + sqlx::query_as!( + Self, + #query, + #id_name, lang + ) + .fetch_all(pool) + .await + } + } + }.into()) + .collect(); + + let table_name = opts.table_name; let name_table_name = opts.name_table_name; 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; }; - let query = format!(r#" + let get_query = format!(r#" SELECT {0}.*, {1}({0}.id, $2) AS name @@ -65,15 +129,33 @@ WHERE {0}.id = $1;"#, table_name, name_func, ); + let all_query = format!(r#" +SELECT + {0}.*, + {1}({0}.id, $1) AS name +FROM {0}"#, + table_name, + name_func, + ); let output = quote! { impl NameTableName for #ident { #answer } impl #ident { + #(#by_many_funcs)* + pub async fn all(pool: &sqlx::PgPool, lang: i32) -> Result, 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, sqlx::Error> { sqlx::query_as!( #ident, - #query, + #get_query, id, lang ) .fetch_optional(pool) diff --git a/src/filters.rs b/src/filters.rs index 3d84195..e534045 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,8 +1,75 @@ +use crate::{ + Player, + ShotDetails, + GoalDetails, + SupportedLanguage, +}; + pub fn seconds_as_time(secs: &i32) -> ::askama::Result { let minutes = secs / 60; let seconds = secs % 60; Ok(format!("{}:{}", minutes, seconds)) } +pub fn player_name(player: &Player) -> ::askama::Result { + Ok(format!("{} {}", initials(&player.first_names)?, &player.last_name)) +} +pub fn goal_player_name(goal: &GoalDetails) -> ::askama::Result { + Ok(format!("{} {}", initials(&goal.player_first_names)?, &goal.player_last_name)) +} +pub fn goal_assist_name(goal: &GoalDetails, lang: &SupportedLanguage) -> ::askama::Result { + 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 { + 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 { + 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 { + 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 { + Ok(format!("{} {}", initials(&shot.player_first_names)?, &shot.player_last_name)) +} +pub fn initials(first_names: &str) -> ::askama::Result { + Ok(first_names + .split_whitespace() + .map(|name| &name[0..1]) + .collect::>() + .join(".")) +} pub fn nullable(ot: &Option) -> ::askama::Result { match ot { Some(t) => Ok(format!("{}", t)), diff --git a/src/main.rs b/src/main.rs index 1cf1db4..322b9e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -113,6 +113,7 @@ struct BoxScoreTemplate<'a> { #[locale] locale: Locale<'a>, goals: Vec, + lang: SupportedLanguage, } #[derive(Template)] @@ -187,6 +188,7 @@ struct ShotsTableTemplate<'a> { #[locale] locale: Locale<'a>, shots: Vec, + lang: SupportedLanguage, } #[derive(Template, TemplateUrl)] @@ -244,14 +246,14 @@ async fn main() { &SupportedLanguage::English.lookup(GameListTemplate::URL_KEY), get(games_for_division_html), ) - //.route( - // &SupportedLanguage::English.lookup(GameScorePageTemplate::URL_KEY), - // get(score_for_game_html), - //) - //.route( - // &SupportedLanguage::French.lookup(GameScorePageTemplate::URL_KEY), - // get(score_for_game_html), - //) + .route( + &SupportedLanguage::English.lookup(GameScorePageTemplate::URL_KEY), + get(score_for_game_html), + ) + .route( + &SupportedLanguage::French.lookup(GameScorePageTemplate::URL_KEY), + get(score_for_game_html), + ) //.route("/:lang/player/:name/", get(player_from_name)) .with_state(state); 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()) .await .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 .unwrap() .unwrap(); @@ -333,7 +335,7 @@ async fn league_html( State(server_config): State, Path(lang): Path, ) -> 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 { lang_links: other_lang_urls!(lang, LeagueListTemplate), locale: lang.into(), @@ -351,7 +353,7 @@ async fn divisions_for_league_html( .await .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 .unwrap(); let html = DivisionListTemplate { @@ -369,14 +371,14 @@ async fn games_for_division_html( State(server_config): State, Path((lang, division_id)): Path<(SupportedLanguage, i32)>, ) -> 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 .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 .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 { locale: lang.into(), lang_links: other_lang_urls!(lang, GameListTemplate, "id" => division_id), @@ -390,22 +392,19 @@ async fn games_for_division_html( }; (StatusCode::OK, games_template) } -/* async fn score_for_game_html( State(server_config): State, Path((lang, game_id)): Path<(SupportedLanguage, i32)>, ) -> impl IntoResponse { - let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id = $1;") - .bind(game_id) - .fetch_one(&*server_config.db_pool) + let game = Game::get(&*server_config.db_pool, game_id, lang.into()) .await - .unwrap(); - let division = Division::get(&*server_config.db_pool, game.division, lang) + .unwrap().unwrap(); + let division = Division::get(&*server_config.db_pool, game.division, lang.into()) .await .unwrap() .unwrap(); - let pbp = game.play_by_play(&server_config.db_pool).await.unwrap(); - let score = game.score(&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, lang.into()).await.unwrap(); let score_html = TeamGameStatsTemplate { locale: lang.into(), teams: score, @@ -415,14 +414,16 @@ async fn score_for_game_html( locale: lang.into(), 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 { locale: lang.into(), goals: box_score, + lang, }; let pbp_html = ShotsTableTemplate { locale: lang.into(), shots: pbp, + lang }; let game_template = GameScorePageTemplate { locale: lang.into(), @@ -437,7 +438,6 @@ async fn score_for_game_html( }; (StatusCode::OK, game_template) } -*/ /* macro_rules! insert { diff --git a/src/model.rs b/src/model.rs index d401685..6f1fe2e 100644 --- a/src/model.rs +++ b/src/model.rs @@ -8,7 +8,7 @@ pub trait TableName { } pub trait NameTableName { 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 { ($ty:ident, $tname:literal) => { @@ -148,7 +148,7 @@ pub struct League { pub name: Option, } //impl_localized_get!(League, league_name); -impl_localized_all!(League); +//impl_localized_all!(League); /* #[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)] #[ormx(table_name = "leagues", table = League, id = "id")] @@ -157,18 +157,18 @@ pub struct NewLeague { } */ -#[derive(FromRow, Serialize, Deserialize, Debug)] -//#[ormx(table = "divisions", id = id, insertable, deletable)] +#[derive(FromRow, Serialize, Deserialize, Debug, NameTableName)] +#[table_names(table_name = "divisions", name_func = "division_name", name_table_name = "division_names", name_table_name_fk = "division")] pub struct Division { //#[ormx(default)] pub id: i32, - //#[ormx(get_many(i32))] + #[table_names(get_many)] pub league: i32, pub name: Option, } -impl_localized_get!(Division, division_name); -impl_localized_get_by_many!(Division, league); -impl_localized_all!(Division); +//impl_localized_get!(Division, division_name); +//impl_localized_get_by_many!(Division, league); +//impl_localized_all!(Division); #[derive(FromRow, Serialize, Deserialize, Debug)] //#[ormx(table_name = "divisions", table = Division, id = "id")] @@ -176,8 +176,9 @@ pub struct NewDivision { pub league: i32, } -#[derive(FromRow, Serialize, Deserialize, Debug)] +#[derive(FromRow, Serialize, Deserialize, Debug, NameTableName)] //#[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 { //#[ormx(default)] pub id: i32, @@ -185,7 +186,6 @@ pub struct Team { pub image: Option, pub name: Option, } -impl_localized_get!(Team, team_name); /* #[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)] @@ -209,10 +209,11 @@ pub struct Player { impl Player { pub async fn from_name_case_insensitive(pool: &sqlx::PgPool, name: String) -> Option { - sqlx::query_as::<_, Player>( - "SELECT * FROM players WHERE REPLACE(UPPER(name), ' ', '-') LIKE UPPER($1);", + sqlx::query_as!( + Player, + "SELECT * FROM players WHERE REPLACE(UPPER(last_name), ' ', '-') LIKE UPPER($1);", + name ) - .bind(name) .fetch_optional(pool) .await .unwrap() @@ -258,19 +259,21 @@ pub struct GamePlayer { pub game: i32, } -#[derive(FromRow, Deserialize, Serialize, Debug)] -//#[ormx(table = "games", id = id, insertable, deletable)] +#[derive(FromRow, Deserialize, Serialize, Debug, NameTableName)] +#[table_names(table_name = "games", name_func = "game_name", name_table_name = "game_names", name_table_name_fk = "game")] pub struct Game { //#[ormx(default)] pub id: i32, - //#[ormx(get_many(i32))] + #[table_names(get_many)] pub division: i32, pub team_home: i32, pub team_away: i32, pub name: Option, + pub start_at: DateTime, + pub end_at: DateTime, } -impl_localized_get!(Game, game_name); -impl_localized_get_by_many!(Game, division); +//impl_localized_get!(Game, game_name); +//impl_localized_get_by_many!(Game, division); #[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)] #[ormx(table = "periods", id = id, insertable, deletable)] @@ -281,20 +284,6 @@ pub struct Period { 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)] mod tests { use crate::languages::SupportedLanguage; @@ -328,7 +317,7 @@ mod tests { 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; + let player = Player::from_name_case_insensitive(&pool, "hoyem".to_string()).await; assert!(player.is_some()); let player = player.unwrap(); assert_eq!(player.first_names, "Tait"); @@ -356,14 +345,7 @@ mod tests { fn $func_name() { tokio_test::block_on(async move { let pool = db_connect().await; - let results = sqlx::query_as::<_, $ret_type>(&format!( - "SELECT * FROM {};", - <$ret_type as TableName>::TABLE_NAME - )) - .fetch_all(&pool) - .await - .unwrap(); - // check that there is at least one result item + let results = $ret_type::all(&pool, SupportedLanguage::English.into()).await.unwrap(); assert!( results.len() > 0, "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!(Player, select_player); + //generate_select_test!(GamePlayer, selec_game_player); + // generate_select_test!(Player, select_player); generate_select_test!(League, select_league); generate_select_test!(Division, select_division); 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!(Language, select_lang); + //generate_select_test!(Language, select_lang); } diff --git a/src/views.rs b/src/views.rs index 373c52e..691b050 100644 --- a/src/views.rs +++ b/src/views.rs @@ -15,7 +15,7 @@ pub struct TeamStats { #[derive(FromRow, Deserialize, Serialize, Debug)] pub struct IihfStats { - pub team_name: String, + pub team_name: Option, pub team_id: i32, pub reg_wins: i32, pub reg_losses: i32, @@ -26,7 +26,7 @@ pub struct IihfStats { } #[derive(FromRow, Deserialize, Serialize, Debug)] pub struct IihfStatsI64 { - pub team_name: String, + pub team_name: Option, pub team_id: i32, pub reg_wins: i64, pub reg_losses: i64, @@ -59,7 +59,8 @@ pub struct IihfPoints { #[derive(FromRow, Deserialize, Serialize, Debug)] pub struct Notification { - pub scorer_name: String, + pub scorer_first_names: String, + pub scorer_last_name: String, pub scorer_number: i32, pub position: String, pub scorer_team_name: String, @@ -69,7 +70,8 @@ pub struct Notification { #[derive(FromRow, Deserialize, Serialize, Debug)] pub struct PlayerStats { - pub name: String, + pub first_names: String, + pub last_name: String, pub goals: i64, pub assists: i64, pub points: i64, @@ -81,7 +83,8 @@ SELECT COUNT(shots.id) AS points, 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, - players.name + players.first_names, + players.last_name FROM game_players JOIN players ON game_players.player = players.id @@ -93,7 +96,8 @@ LEFT JOIN shots WHERE game_players.game=$1 GROUP BY game_players.id, - players.name + players.last_name, + players.first_names HAVING COUNT(shots.id) > 0 ORDER BY points DESC, @@ -104,20 +108,23 @@ ORDER BY .fetch_all(pool) .await } -pub async fn game_goals(pool: &PgPool, game_id: i32) -> Result, sqlx::Error> { +pub async fn game_goals(pool: &PgPool, game_id: i32, lang: i32) -> Result, sqlx::Error> { sqlx::query_as::<_, GoalDetails>( r#" SELECT shots.shooter AS player_id, shots.assistant AS first_assist_id, shots.assistant_second AS second_assist_id, - players.name AS player_name, - p_assist.name AS first_assist_name, - p_assist_second.name AS second_assist_name, + players.first_names AS player_first_names, + players.last_name AS player_last_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, gp_assist.player_number AS first_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, shots.period_time AS time_remaining, period_types.id AS period_id, @@ -141,14 +148,15 @@ pub async fn game_goals(pool: &PgPool, game_id: i32) -> Result, "#, ) .bind(game_id) + .bind(lang) .fetch_all(pool) .await } -pub async fn game_iihf_stats(pool: &PgPool, game_id: i32) -> Result, sqlx::Error> { +pub async fn game_iihf_stats(pool: &PgPool, game_id: i32, lang: i32) -> Result, sqlx::Error> { let query = r#" SELECT 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_loss(games.id, teams.id) AS reg_losses, 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(query) .bind(game_id) + .bind(lang) .fetch_all(pool) .await } @@ -176,11 +185,11 @@ pub async fn game_iihf_stats(pool: &PgPool, game_id: i32) -> Result Result, sqlx::Error> { +pub async fn game_iihf_points(pool: &PgPool, game_id: i32, lang: i32) -> Result, sqlx::Error> { let query = r#" SELECT 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 FROM games JOIN teams @@ -191,16 +200,17 @@ pub async fn game_iihf_points(pool: &PgPool, game_id: i32) -> Result(query) .bind(game_id) + .bind(lang) .fetch_all(pool) .await } /// Returns the number of shots and goals for each team in the game. -pub async fn game_score(pool: &PgPool, game_id: i32) -> Result, sqlx::Error> { +pub async fn game_score(pool: &PgPool, game_id: i32, lang: i32) -> Result, sqlx::Error> { let query = r#" SELECT COUNT(CASE WHEN shots.goal = true THEN shots.id END) AS goals, COUNT(shots.id) AS shots, - teams.name AS name + team_name(teams.id, $2) AS name FROM games JOIN periods ON periods.game=games.id JOIN shots ON shots.period=periods.id @@ -211,12 +221,14 @@ pub async fn game_score(pool: &PgPool, game_id: i32) -> Result, s "#; sqlx::query_as::<_, TeamStats>(query) .bind(game_id) + .bind(lang) .fetch_all(pool) .await } pub async fn game_play_by_play( pool: &PgPool, game_id: i32, + lang: i32, ) -> Result, sqlx::Error> { sqlx::query_as::<_, ShotDetails>( r#" @@ -225,13 +237,16 @@ SELECT shots.assistant AS first_assist_id, shots.assistant_second AS second_assist_id, shots.goal AS is_goal, - players.name AS player_name, - p_assistant.name AS first_assist_name, - p_assistant_second.name AS second_assist_name, + players.first_names AS player_first_names, + players.last_name AS player_last_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, gp_assistant.player_number AS first_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, shots.period_time AS time_remaining, period_types.id AS period_id, @@ -253,79 +268,93 @@ ORDER BY "#, ) .bind(game_id) + .bind(lang) .fetch_all(pool) .await } impl Game { - pub async fn score(&self, pool: &PgPool) -> Result, sqlx::Error> { - game_score(pool, self.id).await + pub async fn score(&self, pool: &PgPool, lang: i32) -> Result, sqlx::Error> { + game_score(pool, self.id, lang).await } pub async fn box_score(&self, pool: &PgPool) -> Result, sqlx::Error> { game_box_score(pool, self.id).await } - pub async fn iihf_points(&self, pool: &PgPool) -> Result, sqlx::Error> { - game_iihf_points(pool, self.id).await + pub async fn iihf_points(&self, pool: &PgPool, lang: i32) -> Result, sqlx::Error> { + game_iihf_points(pool, self.id, lang).await } - pub async fn iihf_stats(&self, pool: &PgPool) -> Result, sqlx::Error> { - game_iihf_stats(pool, self.id).await + pub async fn iihf_stats(&self, pool: &PgPool, lang: i32) -> Result, sqlx::Error> { + game_iihf_stats(pool, self.id, lang).await } - pub async fn goals(&self, pool: &PgPool) -> Result, sqlx::Error> { - game_goals(pool, self.id).await + pub async fn goals(&self, pool: &PgPool, lang: i32) -> Result, sqlx::Error> { + game_goals(pool, self.id, lang).await } - pub async fn play_by_play(&self, pool: &PgPool) -> Result, sqlx::Error> { - game_play_by_play(pool, self.id).await + pub async fn play_by_play(&self, pool: &PgPool, lang: i32) -> Result, sqlx::Error> { + game_play_by_play(pool, self.id, lang).await } } pub async fn division_iihf_stats( pool: &PgPool, division_id: i32, - lang: SupportedLanguage, + lang: i32, ) -> Result, sqlx::Error> { - sqlx::query_as::<_, IihfStatsI64>( + sqlx::query_as!( + IihfStatsI64, 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 - 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; + SUM(points) AS "points!", + SUM(reg_wins) AS "reg_wins!", + SUM(reg_losses) AS "reg_losses!", + SUM(ot_wins) AS "ot_wins!", + SUM(ot_losses) AS "ot_losses!", + SUM(ties) AS "ties!", + team_name(team_id, $2) AS team_name, + team_id AS "team_id!" +FROM team_points_view +WHERE division_id=$1 +GROUP BY team_id; +--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 +-- 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) -- SUM(reg_win(games.id, teams.id)) AS reg_wins, @@ -361,23 +390,22 @@ ORDER BY -- teams.id, -- points DESC; "#, + division_id, lang ) - .bind(division_id) - .bind(lang) .fetch_all(pool) .await } impl Division { - pub async fn iihf_stats(&self, pool: &PgPool, lang: SupportedLanguage) -> Result, sqlx::Error> { + pub async fn iihf_stats(&self, pool: &PgPool, lang: i32) -> Result, sqlx::Error> { division_iihf_stats(pool, self.id, lang).await } } impl Player { - pub async fn latest_league(pool: &PgPool, id: i32) -> Result, sqlx::Error> { + pub async fn latest_league(pool: &PgPool, id: i32, lang: i32) -> Result, sqlx::Error> { let query = r#" - SELECT leagues.* + SELECT leagues.*,team_name(teams.id, $2) AS name FROM players JOIN game_players ON game_players.player=players.id JOIN games ON games.id=game_players.game @@ -390,22 +418,26 @@ impl Player { "#; sqlx::query_as::<_, League>(query) .bind(id) + .bind(lang) .fetch_optional(pool) .await } - pub async fn latest_stats(pool: &PgPool, id: i32) -> Result, sqlx::Error> { + pub async fn latest_stats(pool: &PgPool, id: i32, lang: i32) -> Result, sqlx::Error> { let query = r#" SELECT players.id AS player_id, p_assist.id AS first_assist_id, p_assist_second.id AS second_assist_id, - players.name AS player_name, - p_assist.name AS first_assist_name, - p_assist_second.name AS second_assist_name, + players.first_names AS player_first_names, + players.last_name AS player_last_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, gp_assist.player_number AS first_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, shots.period_time AS time_remaining, period_types.id AS period_id, @@ -429,6 +461,7 @@ impl Player { "#; sqlx::query_as::<_, GoalDetails>(&query) .bind(id) + .bind(lang) .fetch_all(pool) .await } @@ -438,7 +471,8 @@ impl Player { COUNT(goals) AS goals, COUNT(assists) AS assists, COUNT(points) AS points, - players.name AS name + players.first_names AS first_names, + players.last_name AS last_name FROM players JOIN game_players ON game_players.player=players.id LEFT JOIN shots points @@ -468,7 +502,8 @@ SELECT COUNT(shots.id) AS points, 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, - players.name + players.first_names AS first_names, + players.last_name AS last_name FROM game_players JOIN players ON game_players.player = players.id LEFT JOIN shots @@ -478,7 +513,8 @@ LEFT JOIN shots OR shots.assistant_second=game_players.id) GROUP BY game_players.id, - players.name + players.first_names, + players.last_name ORDER BY points DESC, goals DESC; @@ -499,7 +535,8 @@ impl League { COUNT(goals.id) AS goals, COUNT(assists.id) AS assists, COUNT(points.id) AS points, - players.name AS name + players.first_names AS first_names, + players.last_name AS last_name FROM players JOIN game_players ON game_players.player=players.id LEFT JOIN shots goals @@ -532,17 +569,20 @@ impl League { #[derive(FromRow, Deserialize, Serialize, Debug)] pub struct GoalDetails { pub player_id: i32, - pub player_name: String, + pub player_first_names: String, + pub player_last_name: String, pub player_number: i32, pub team_name: String, pub team_id: i32, pub time_remaining: i32, pub period_id: i32, pub period_short_name: String, - pub first_assist_name: Option, + pub first_assist_first_names: Option, + pub first_assist_last_name: Option, pub first_assist_number: Option, pub first_assist_id: Option, - pub second_assist_name: Option, + pub second_assist_first_names: Option, + pub second_assist_last_name: Option, pub second_assist_id: Option, pub second_assist_number: Option, } @@ -550,17 +590,20 @@ pub struct GoalDetails { #[derive(FromRow, Deserialize, Serialize, Debug)] pub struct ShotDetails { pub player_id: i32, - pub player_name: String, + pub player_first_names: String, + pub player_last_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, + pub first_assist_first_names: Option, + pub first_assist_last_name: Option, pub first_assist_number: Option, pub first_assist_id: Option, - pub second_assist_name: Option, + pub second_assist_first_names: Option, + pub second_assist_last_name: Option, pub second_assist_id: Option, pub second_assist_number: Option, } @@ -580,7 +623,7 @@ mod tests { fn check_play_by_play() { tokio_test::block_on(async move { 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 { let pool = db_connect().await; 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() { tokio_test::block_on(async move { 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 stats = League::player_stats(&pool, player.id, league.id) .await .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 { let pool = db_connect().await; 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 .unwrap() .unwrap(); @@ -623,7 +666,7 @@ mod tests { fn check_score_details_from_game() { tokio_test::block_on(async move { 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:?}"); }) } @@ -635,7 +678,7 @@ mod tests { let scores = game_box_score(&pool, 4).await.unwrap(); println!("{scores:?}"); let second_top_scorer = scores.get(1).unwrap(); - assert_eq!(second_top_scorer.name, "Allyssa Foulds"); + assert_eq!(second_top_scorer.last_name, "Foulds"); assert_eq!(second_top_scorer.goals, 1, "Allysa should have 1 goal.."); assert_eq!( second_top_scorer.assists, 2, @@ -654,14 +697,16 @@ mod tests { fn check_division_iihf_stats() { tokio_test::block_on(async move { 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.get(0).unwrap().points, 10, "Top team should have 10 points"); - assert_eq!(score.get(0).unwrap().team_name, "Bullseye", "Top team should be bullseye"); - assert_eq!(score.get(0).unwrap().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!(score.get(1).unwrap().team_name, "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_1.points, 10, "Top team should have 10 points"); + assert_eq!(team_1.team_name.as_ref().unwrap(), "Bullseye", "Top team should be bullseye"); + assert_eq!(team_1.reg_losses, 0, "The bullseye should have no regulation losses"); + assert_eq!(team_1.ties, 2, "There should be two ties for the bullsye"); + assert_eq!(team_2.team_name.as_ref().unwrap(), "See Cats", "The second-place team should be the see cats"); + assert_eq!(team_2.points, 4, "The second-place team should have four points"); }) } @@ -669,11 +714,12 @@ mod tests { fn check_iihf_stats() { tokio_test::block_on(async move { let pool = db_connect().await; - let score = game_iihf_stats(&pool, 4).await.unwrap(); - assert_eq!(score.get(0).unwrap().points, 2); - assert_eq!(score.get(0).unwrap().team_name, "Bullseye"); - assert_eq!(score.get(0).unwrap().reg_losses, 0); - assert_eq!(score.get(0).unwrap().ties, 1); + let score = game_iihf_stats(&pool, 4, SupportedLanguage::English.into()).await.unwrap(); + let team_1 = score.get(0).unwrap(); + assert_eq!(team_1.points, 2); + assert_eq!(team_1.team_name.as_ref().unwrap(), "Bullseye"); + assert_eq!(team_1.reg_losses, 0); + assert_eq!(team_1.ties, 1); assert_eq!(score.get(1).unwrap().points, 2); }) } @@ -682,7 +728,7 @@ mod tests { fn check_iihf_points() { tokio_test::block_on(async move { 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().team_name, "Bullseye"); assert_eq!(score.get(1).unwrap().points, 2); @@ -693,7 +739,7 @@ mod tests { fn check_game_score() { tokio_test::block_on(async move { 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(1).unwrap().goals, 1); }) @@ -724,8 +770,9 @@ mod tests { let pool = db_connect().await; let query = r#" SELECT - teams.name AS scorer_team_name, - players.name AS scorer_name, + team_name(teams.id, $1) AS scorer_team_name, + players.first_names AS scorer_first_names, + players.last_name AS scorer_last_name, positions.name AS position, game_players.player_number AS scorer_number, 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; "#; let result = sqlx::query_as::<_, Notification>(query) + .bind(1) .fetch_one(&pool) .await .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}", result.scorer_team_name, result.position, - result.scorer_name, + result.scorer_last_name, result.scorer_number, minutes, seconds, diff --git a/templates/game_list.html b/templates/game_list.html index 248d055..778acef 100644 --- a/templates/game_list.html +++ b/templates/game_list.html @@ -1,6 +1,6 @@ {% 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 %}

Division: {{ division.name|nullable }}

diff --git a/templates/game_score_page.html b/templates/game_score_page.html index ed3f3de..5c590b8 100644 --- a/templates/game_score_page.html +++ b/templates/game_score_page.html @@ -1,6 +1,6 @@ {% extends "master.html" %} -{% block title %}{{ game.name.clone().unwrap() }}{% endblock %} +{% block title %}{{ game.name|nullable }}{% endblock %} {% block content %}

{{ localize("game-of-division", game: game.name.clone().unwrap(), division: division.name.clone().unwrap()) }}

diff --git a/templates/partials/box_score_table.html b/templates/partials/box_score_table.html index 6475407..d07d18d 100644 --- a/templates/partials/box_score_table.html +++ b/templates/partials/box_score_table.html @@ -11,24 +11,16 @@ {% for goal in goals %} - {{ goal.player_name }} + {{ goal|goal_player_name }} {{ goal.team_name }} {{ goal.player_number }} {{ goal.period_short_name }} {{ goal.time_remaining|seconds_as_time }} - {% if goal.first_assist_name.is_some() %} - {{ goal.first_assist_name.as_ref().unwrap() }} - {% else %} - {{ localize("unassisted") }} - {% endif %} + {{ goal|goal_assist_name(lang) }} - {% if goal.second_assist_name.is_some() %} - {{ goal.second_assist_name.as_ref().unwrap() }} - {% else %} - {{ localize("not-applicable") }} - {% endif %} + {{ goal|goal_second_assist_name(lang) }} {% endfor %} diff --git a/templates/partials/iihf_team_stats_table.html b/templates/partials/iihf_team_stats_table.html index 5b487d8..c0113f8 100644 --- a/templates/partials/iihf_team_stats_table.html +++ b/templates/partials/iihf_team_stats_table.html @@ -13,7 +13,7 @@ {% for team in iihf_stats %} - {{ team.team_name }} + {{ team.team_name|nullable }} {{ team.points }} {{ team.reg_wins }} {{ team.reg_losses }} diff --git a/templates/partials/individual_game_points_table.html b/templates/partials/individual_game_points_table.html index d7e0738..f0f861b 100644 --- a/templates/partials/individual_game_points_table.html +++ b/templates/partials/individual_game_points_table.html @@ -10,7 +10,7 @@ {% for player in players %} - {{ player.name }} + {{ player.first_names|initials }} {{ player.last_name }} {{ player.points }} {{ player.goals }} {{ player.assists }} diff --git a/templates/partials/play_by_play_table.html b/templates/partials/play_by_play_table.html index 09ef543..9a8eda3 100644 --- a/templates/partials/play_by_play_table.html +++ b/templates/partials/play_by_play_table.html @@ -12,7 +12,7 @@ {% for shot in shots %} - {{ shot.player_name }} + {{ shot.player_first_names|initials }} {{ shot.player_last_name }} {{ shot.team_name }} {{ shot.player_number }} @@ -25,22 +25,10 @@ {{ shot.period_short_name }} {{ shot.time_remaining|seconds_as_time }} - {% if shot.is_goal %} - {% if shot.first_assist_name.is_some() %} - {{ shot.first_assist_name.as_ref().unwrap() }} - {% else %} - {{ localize("unassisted") }} - {% endif %} - {% else %} - {{ localize("not-applicable") }} - {% endif %} + {{ shot|shot_assist_name(lang) }} - {% if shot.second_assist_name.is_some() %} - {{ shot.second_assist_name.as_ref().unwrap() }} - {% else %} - {{ localize("not-applicable") }} - {% endif %} + {{ shot|shot_second_assist_name(lang) }} {% endfor %}