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 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<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))]
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<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 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<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> {
sqlx::query_as!(
#ident,
#query,
#get_query,
id, lang
)
.fetch_optional(pool)

@ -1,8 +1,75 @@
use crate::{
Player,
ShotDetails,
GoalDetails,
SupportedLanguage,
};
pub fn seconds_as_time(secs: &i32) -> ::askama::Result<String> {
let minutes = secs / 60;
let seconds = secs % 60;
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> {
match ot {
Some(t) => Ok(format!("{}", t)),

@ -113,6 +113,7 @@ struct BoxScoreTemplate<'a> {
#[locale]
locale: Locale<'a>,
goals: Vec<GoalDetails>,
lang: SupportedLanguage,
}
#[derive(Template)]
@ -187,6 +188,7 @@ struct ShotsTableTemplate<'a> {
#[locale]
locale: Locale<'a>,
shots: Vec<ShotDetails>,
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<ServerState>,
Path(lang): Path<SupportedLanguage>,
) -> 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<ServerState>,
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<ServerState>,
Path((lang, game_id)): Path<(SupportedLanguage, i32)>,
) -> impl IntoResponse {
let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id = $1;")
.bind(game_id)
.fetch_one(&*server_config.db_pool)
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 {

@ -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<String>,
}
//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<String>,
}
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<String>,
pub name: Option<String>,
}
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<Player> {
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<String>,
pub start_at: DateTime<Utc>,
pub end_at: DateTime<Utc>,
}
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);
}

@ -15,7 +15,7 @@ pub struct TeamStats {
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct IihfStats {
pub team_name: String,
pub team_name: Option<String>,
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<String>,
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<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>(
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<Vec<GoalDetails>,
"#,
)
.bind(game_id)
.bind(lang)
.fetch_all(pool)
.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#"
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<Vec<IihfStat
"#;
sqlx::query_as::<_, IihfStats>(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<Vec<IihfStat
/// NOTE: The algorithm used here requires that a 4th period is the "overtime";
/// it does not check if there was only two periods, followed by an overtime.
/// This should be sufficient for most.
pub async fn 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#"
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<Vec<IihfPoi
"#;
sqlx::query_as::<_, IihfPoints>(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<Vec<TeamStats>, sqlx::Error> {
pub async fn game_score(pool: &PgPool, game_id: i32, lang: i32) -> Result<Vec<TeamStats>, 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<Vec<TeamStats>, 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<Vec<ShotDetails>, 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<Vec<TeamStats>, sqlx::Error> {
game_score(pool, self.id).await
pub async fn score(&self, pool: &PgPool, lang: i32) -> Result<Vec<TeamStats>, sqlx::Error> {
game_score(pool, self.id, lang).await
}
pub async fn box_score(&self, pool: &PgPool) -> Result<Vec<PlayerStats>, sqlx::Error> {
game_box_score(pool, self.id).await
}
pub async fn iihf_points(&self, pool: &PgPool) -> Result<Vec<IihfPoints>, sqlx::Error> {
game_iihf_points(pool, self.id).await
pub async fn iihf_points(&self, pool: &PgPool, lang: i32) -> Result<Vec<IihfPoints>, sqlx::Error> {
game_iihf_points(pool, self.id, lang).await
}
pub async fn iihf_stats(&self, pool: &PgPool) -> Result<Vec<IihfStats>, sqlx::Error> {
game_iihf_stats(pool, self.id).await
pub async fn iihf_stats(&self, pool: &PgPool, lang: i32) -> Result<Vec<IihfStats>, sqlx::Error> {
game_iihf_stats(pool, self.id, lang).await
}
pub async fn goals(&self, pool: &PgPool) -> Result<Vec<GoalDetails>, sqlx::Error> {
game_goals(pool, self.id).await
pub async fn goals(&self, pool: &PgPool, lang: i32) -> Result<Vec<GoalDetails>, sqlx::Error> {
game_goals(pool, self.id, lang).await
}
pub async fn play_by_play(&self, pool: &PgPool) -> Result<Vec<ShotDetails>, sqlx::Error> {
game_play_by_play(pool, self.id).await
pub async fn play_by_play(&self, pool: &PgPool, lang: i32) -> Result<Vec<ShotDetails>, 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<Vec<IihfStatsI64>, 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<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
}
}
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#"
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<Vec<GoalDetails>, sqlx::Error> {
pub async fn latest_stats(pool: &PgPool, id: i32, lang: i32) -> Result<Vec<GoalDetails>, 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<String>,
pub first_assist_first_names: Option<String>,
pub first_assist_last_name: Option<String>,
pub first_assist_number: 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_number: Option<i32>,
}
@ -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<String>,
pub first_assist_first_names: Option<String>,
pub first_assist_last_name: Option<String>,
pub first_assist_number: 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_number: Option<i32>,
}
@ -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,

@ -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 %}
<h1>Division: {{ division.name|nullable }}</h1>

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

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

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

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

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

Loading…
Cancel
Save