From b0b0e8c33d1124b901f5966bd32c51367b4e6335 Mon Sep 17 00:00:00 2001 From: Tait Hoyem Date: Thu, 30 Mar 2023 13:05:38 -0600 Subject: [PATCH] First MVP --- migrations/20230324230201_create_games.up.sql | 5 +- ...0230327021921_create_period_types.down.sql | 1 + ...20230327021921_create_period_types.up.sql} | 6 +- .../20230327021922_add_period_types.down.sql | 1 + .../20230327021922_add_period_types.up.sql | 16 + migrations/20230327021922_add_periods.up.sql | 16 - ...=> 20230327021923_create_periods.down.sql} | 0 .../20230327021923_create_periods.up.sql | 19 + migrations/20230327224840_create_shots.up.sql | 11 +- ...ql => 20230327235842_add_periods.down.sql} | 0 migrations/20230327235842_add_periods.up.sql | 63 + migrations/20230327235843_add_shots.up.sql | 1210 ++++++++++++++++- src/filters.rs | 5 + src/main.rs | 248 ++-- src/model.rs | 74 +- src/views.rs | 518 ++++++- templates/division_list.html | 6 + templates/game_list.html | 10 + templates/game_score_page.html | 9 + templates/hello.html | 2 + templates/league_list.html | 6 + templates/partials/box_score_table.html | 36 + .../individual_game_points_table.html | 20 + templates/partials/play_by_play_table.html | 48 + templates/partials/team_stats_table.html | 18 + templates/player_page.html | 17 + 26 files changed, 2133 insertions(+), 232 deletions(-) create mode 100644 migrations/20230327021921_create_period_types.down.sql rename migrations/{20230327021921_create_periods.up.sql => 20230327021921_create_period_types.up.sql} (66%) create mode 100644 migrations/20230327021922_add_period_types.down.sql create mode 100644 migrations/20230327021922_add_period_types.up.sql delete mode 100644 migrations/20230327021922_add_periods.up.sql rename migrations/{20230327021921_create_periods.down.sql => 20230327021923_create_periods.down.sql} (100%) create mode 100644 migrations/20230327021923_create_periods.up.sql rename migrations/{20230327021922_add_periods.down.sql => 20230327235842_add_periods.down.sql} (100%) create mode 100644 migrations/20230327235842_add_periods.up.sql create mode 100644 src/filters.rs create mode 100644 templates/division_list.html create mode 100644 templates/game_list.html create mode 100644 templates/game_score_page.html create mode 100644 templates/hello.html create mode 100644 templates/league_list.html create mode 100644 templates/partials/box_score_table.html create mode 100644 templates/partials/individual_game_points_table.html create mode 100644 templates/partials/play_by_play_table.html create mode 100644 templates/partials/team_stats_table.html create mode 100644 templates/player_page.html diff --git a/migrations/20230324230201_create_games.up.sql b/migrations/20230324230201_create_games.up.sql index 39d8530..c07ab51 100644 --- a/migrations/20230324230201_create_games.up.sql +++ b/migrations/20230324230201_create_games.up.sql @@ -1,8 +1,9 @@ -- Add up migration script here CREATE TABLE IF NOT EXISTS games ( id SERIAL PRIMARY KEY NOT NULL, - -- a possibly null name for the game; this allows there to be special names like "Gold Medal Game" - name VARCHAR(255), + -- this allows there to be special names like "Gold Medal Game", but the default will be the number of games already in the division + 1 + -- NOTE: this is only done in the front end, the backend will not give a default value + name VARCHAR(255) NOT NULL, -- what divison is the game a part of (usefl for stats) division INTEGER NOT NULL, team_home INTEGER NOT NULL, diff --git a/migrations/20230327021921_create_period_types.down.sql b/migrations/20230327021921_create_period_types.down.sql new file mode 100644 index 0000000..7cb191f --- /dev/null +++ b/migrations/20230327021921_create_period_types.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS period_types; diff --git a/migrations/20230327021921_create_periods.up.sql b/migrations/20230327021921_create_period_types.up.sql similarity index 66% rename from migrations/20230327021921_create_periods.up.sql rename to migrations/20230327021921_create_period_types.up.sql index ec99691..d49ca59 100644 --- a/migrations/20230327021921_create_periods.up.sql +++ b/migrations/20230327021921_create_period_types.up.sql @@ -1,8 +1,10 @@ -CREATE TABLE IF NOT EXISTS periods ( +CREATE TABLE IF NOT EXISTS period_types ( id SERIAL PRIMARY KEY NOT NULL, -- "first", "second", "third", "second overtime", "shootout" name VARCHAR(32) NOT NULL, -- "1", "2", "3", "OT", "[2-9]OT", "SO" -- technically 10+OT would not work, but this should be rare enough to not worry about. - short_name VARCHAR(3) NOT NULL + short_name VARCHAR(3) NOT NULL, + -- default length + default_length INTEGER NOT NULL ); diff --git a/migrations/20230327021922_add_period_types.down.sql b/migrations/20230327021922_add_period_types.down.sql new file mode 100644 index 0000000..b5f0b53 --- /dev/null +++ b/migrations/20230327021922_add_period_types.down.sql @@ -0,0 +1 @@ +DELETE FROM period_types; diff --git a/migrations/20230327021922_add_period_types.up.sql b/migrations/20230327021922_add_period_types.up.sql new file mode 100644 index 0000000..c124ed9 --- /dev/null +++ b/migrations/20230327021922_add_period_types.up.sql @@ -0,0 +1,16 @@ +INSERT INTO period_types + (id, name, short_name, default_length) +VALUES + (1, 'first', '1', 1200), + (2, 'second', '2', 1200), + (3, 'third', '3', 1200), + (4, 'overtime', 'OT', 300), + (5, 'shootout', 'SO', 0), + (6, 'second overtime', '2OT', 1200), + (7, 'third overtime', '3OT', 1200), + (8, 'fourth overtime', '4OT', 1200), + (9, 'fifth overtime', '5OT', 1200), + (10, 'sixth overtime', '6OT', 1200), + (11, 'seventh overtime', '7OT', 1200), + (12, 'eighth overtime', '8OT', 1200), + (13, 'ninth overtime', '9OT', 1200); diff --git a/migrations/20230327021922_add_periods.up.sql b/migrations/20230327021922_add_periods.up.sql deleted file mode 100644 index 41aeb7d..0000000 --- a/migrations/20230327021922_add_periods.up.sql +++ /dev/null @@ -1,16 +0,0 @@ -INSERT INTO periods - (id, name, short_name) -VALUES - (1, 'first', '1'), - (2, 'second', '2'), - (3, 'third', '3'), - (4, 'overtime', 'OT'), - (5, 'shootout', 'SO'), - (6, 'second overtime', '2OT'), - (7, 'third overtime', '3OT'), - (8, 'fourth overtime', '4OT'), - (9, 'fifth overtime', '5OT'), - (10, 'sixth overtime', '6OT'), - (11, 'seventh overtime', '7OT'), - (12, 'eighth overtime', '8OT'), - (13, 'ninth overtime', '9OT'); diff --git a/migrations/20230327021921_create_periods.down.sql b/migrations/20230327021923_create_periods.down.sql similarity index 100% rename from migrations/20230327021921_create_periods.down.sql rename to migrations/20230327021923_create_periods.down.sql diff --git a/migrations/20230327021923_create_periods.up.sql b/migrations/20230327021923_create_periods.up.sql new file mode 100644 index 0000000..6ba0941 --- /dev/null +++ b/migrations/20230327021923_create_periods.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS periods ( + id SERIAL PRIMARY KEY NOT NULL, + -- which kind of period is it: 1st, 2nd, third, SO, OT, 5OT, etc. + period_type INTEGER NOT NULL, + -- length of period in seconds + period_length INTEGER NOT NULL, + -- which game does this period refer to + game INTEGER NOT NULL, + -- period type must exists + CONSTRAINT period_type_fk + FOREIGN KEY(period_type) + REFERENCES period_types(id) + ON DELETE RESTRICT, + -- game must exist + CONSTRAINT game_fk + FOREIGN KEY(game) + REFERENCES games(id) + ON DELETE RESTRICT +); diff --git a/migrations/20230327224840_create_shots.up.sql b/migrations/20230327224840_create_shots.up.sql index 496b1a3..46d2718 100644 --- a/migrations/20230327224840_create_shots.up.sql +++ b/migrations/20230327224840_create_shots.up.sql @@ -16,8 +16,6 @@ CREATE TABLE IF NOT EXISTS shots ( shooter INTEGER NOT NULL, -- which player was the goalie goalie INTEGER NOT NULL, - -- which game was this a part of - game INTEGER NOT NULL, -- which period did the shot happen in period INTEGER NOT NULL, -- when did the shot happen relative to the beginning of the period @@ -25,6 +23,8 @@ CREATE TABLE IF NOT EXISTS shots ( -- if applicable, set assistant(s) assistant INTEGER, assistant_second INTEGER, + -- when was the record created + created_at TIMESTAMPTZ NOT NULL DEFAULT current_timestamp, -- was the shooter a real player CONSTRAINT shooter_fk FOREIGN KEY(shooter) @@ -55,12 +55,7 @@ CREATE TABLE IF NOT EXISTS shots ( FOREIGN KEY(shooter_team) REFERENCES teams(id) ON DELETE RESTRICT, - -- is the game a real game - CONSTRAINT game_fk - FOREIGN KEY(game) - REFERENCES games(id) - ON DELETE RESTRICT, - -- is the period refgerences a real period type + -- is the period references a real period CONSTRAINT period_fk FOREIGN KEY(period) REFERENCES periods(id) diff --git a/migrations/20230327021922_add_periods.down.sql b/migrations/20230327235842_add_periods.down.sql similarity index 100% rename from migrations/20230327021922_add_periods.down.sql rename to migrations/20230327235842_add_periods.down.sql diff --git a/migrations/20230327235842_add_periods.up.sql b/migrations/20230327235842_add_periods.up.sql new file mode 100644 index 0000000..c69f100 --- /dev/null +++ b/migrations/20230327235842_add_periods.up.sql @@ -0,0 +1,63 @@ +INSERT INTO periods + (game, period_type, period_length) +VALUES + ( + 1, + 1, + 1200 + ), + ( + 1, + 2, + 900 + ), + ( + 1, + 3, + 900 + ), + ( + 2, + 1, + 1200 + ), + ( + 2, + 2, + 1200 + ), + ( + 2, + 3, + 1200 + ), + ( + 3, + 1, + 720 + ), + ( + 3, + 2, + 720 + ), + ( + 3, + 3, + 1200 + ), + ( + 4, + 1, + 1200 + ), + ( + 4, + 2, + 1200 + ), + ( + 4, + 3, + 1200 + ); diff --git a/migrations/20230327235843_add_shots.up.sql b/migrations/20230327235843_add_shots.up.sql index 14601a9..9f2bae7 100644 --- a/migrations/20230327235843_add_shots.up.sql +++ b/migrations/20230327235843_add_shots.up.sql @@ -1,94 +1,999 @@ -- Add up migration script here INSERT INTO shots - (shooter_team, goalie, shooter, game, period, period_time, video_timestamp, assistant, assistant_second, on_net, goal) + (shooter_team, goalie, shooter, period, period_time, video_timestamp, assistant, assistant_second, on_net, goal) VALUES + ( + 1, + 12, + 1, + 1, + 1018, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 18, + 1, + 912, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 1, + 736, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 14, + 1, + 638, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 1, + 495, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 1, + 325, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 21, + 1, + 221, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 1, + 1, + 171, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 9, + 1, + 39, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 2, + 742, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 23, + 2, + 733, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 2, + 676, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 4, + 2, + 651, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 10, + 2, + 334, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 2, + 321, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 14, + 2, + 243, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 2, + 174, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 2, + 160, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 3, + 517, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 14, + 3, + 503, + null, + 16, + null, + true, + true + ), + ( + 1, + 12, + 1, + 3, + 433, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 22, + 3, + 297, + null, + null, + null, + true, + false + ), + ( + 2, + 3, + 23, + 3, + 212, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 3, + 30, + null, + null, + null, + true, + false + ), + ( + 1, + 12, + 7, + 3, + 26, + null, + null, + null, + true, + true + ), + ( + 2, + 3, + 22, + 4, + 1120, + 329, + null, + null, + true, + false + ), + ( + 2, + 3, + 15, + 4, + 828, + 621, + null, + null, + true, + false + ), + ( + 1, + 12, + 4, + 4, + 465, + 985, + null, + null, + true, + false + ), + ( + 2, + 3, + 22, + 4, + 107, + 1343, + null, + null, + true, + false + ), + ( + 1, + 12, + 9, + 4, + 47, + 1402, + null, + null, + true, + false + ), + ( + 1, + 13, + 31, + 5, + 1186, + 1649, + 2, + null, + true, + true + ), + ( + 2, + 3, + 22, + 5, + 1082, + 1754, + null, + null, + true, + false + ), + ( + 1, + 13, + 2, + 5, + 990, + 1845, + null, + null, + true, + false + ), + ( + 1, + 13, + 11, + 5, + 870, + 1966, + null, + null, + true, + false + ), + ( + 1, + 13, + 1, + 5, + 778, + 2059, + null, + null, + true, + false + ), + ( + 1, + 13, + 7, + 5, + 632, + 2203, + null, + null, + true, + false + ), + ( + 1, + 13, + 2, + 5, + 505, + 2451, + null, + null, + true, + false + ), + ( + 2, + 3, + 14, + 5, + 300, + 2536, + null, + null, + true, + false + ), + ( + 2, + 3, + 14, + 5, + 262, + 2574, + null, + null, + true, + false + ), + ( + 1, + 13, + 7, + 5, + 168, + 2668, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 5, + 77, + 2758, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 5, + 74, + 2761, + null, + null, + true, + false + ), + ( + 1, + 12, + 1, + 6, + 1144, + 3101, + null, + null, + true, + false + ), + ( + 1, + 12, + 9, + 6, + 1048, + 3197, + null, + null, + true, + false + ), + ( + 1, + 12, + 9, + 6, + 979, + 3265, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 6, + 875, + 3369, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 6, + 738, + 3507, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 6, + 673, + 3572, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 6, + 621, + 3623, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 6, + 591, + 3654, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 6, + 570, + 3675, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 6, + 552, + 3693, + null, + null, + true, + false + ), + ( + 1, + 12, + 1, + 6, + 22, + 4402, + null, + null, + true, + false + ), + ( + 2, + 3, + 22, + 7, + 657, + 1268, + 21, + null, + true, + true + ), + ( + 1, + 13, + 2, + 7, + 565, + 1360, + 7, + null, + true, + true + ), + ( + 1, + 13, + 2, + 7, + 385, + 1539, + null, + null, + true, + false + ), + ( + 1, + 13, + 1, + 7, + 173, + 1751, + null, + null, + true, + false + ), + ( + 1, + 13, + 7, + 7, + 99, + 1825, + null, + null, + true, + false + ), + ( + 1, + 13, + 1, + 7, + 74, + 1850, + null, + null, + true, + false + ), + ( + 1, + 13, + 2, + 7, + 70, + 1853, + null, + null, + true, + false + ), + ( + 1, + 13, + 1, + 7, + 68, + 1855, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 8, + 675, + 2120, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 8, + 540, + 2255, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 8, + 512, + 2282, + null, + null, + true, + false + ), + ( + 1, + 12, + 1, + 8, + 470, + 2324, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 8, + 412, + 2382, + null, + null, + true, + false + ), + ( + 2, + 3, + 17, + 8, + 252, + 2543, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 8, + 162, + 2677, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 8, + 100, + 2139, + null, + null, + true, + false + ), + ( + 2, + 3, + 21, + 8, + 1042, + 3158, + null, + null, + true, + false + ), ( 2, 3, - 14, + 21, + 8, + 832, + 3367, + null, + null, + true, + false + ), + ( 1, - 3, - 503, + 12, + 2, + 9, + 549, + 3651, null, - 16, null, true, - true + false ), ( 1, 12, - 7, 1, - 3, - 26, + 9, + 282, + 3918, null, - 1, null, true, true ), ( 1, - 13, - 31, - 2, - 2, - 1186, - 1649, + 12, 2, + 9, + 153, + 4047, + null, null, true, - true + false ), ( 2, 3, 22, - 3, + 10, + 1094, + 259, + null, + null, + true, + false + ), + ( 1, - 657, - 1268, - 21, + 12, + 7, + 10, + 1027, + 326, + null, null, true, - true + false ), ( 1, - 13, + 12, 2, - 3, + 10, + 888, + 466, + null, + null, + true, + false + ), + ( 1, - 565, - 1360, - 7, + 12, + 2, + 10, + 879, + 474, + null, null, true, - true + false + ), + ( + 1, + 12, + 2, + 10, + 873, + 481, + null, + null, + true, + false ), ( 1, 12, + 7, + 10, + 836, + 518, + null, + null, + true, + false + ), + ( 1, - 3, - 3, - 282, - 3918, + 12, + 9, + 10, + 794, + 560, null, null, true, - true + false + ), + ( + 2, + 12, + 22, + 10, + 754, + 600, + null, + null, + true, + false ), ( 2, 3, 18, - 4, - 1, + 10, 691, 663, - 1, + null, null, true, true @@ -96,9 +1001,20 @@ VALUES ( 1, 12, - 2, - 4, + 9, + 10, + 164, + 1190, + null, + null, + true, + false + ), + ( 1, + 12, + 2, + 10, 106, 1247, 31, @@ -106,12 +1022,47 @@ VALUES true, true ), + ( + 2, + 3, + 21, + 10, + 19, + 1335, + null, + null, + true, + false + ), + ( + 1, + 13, + 31, + 11, + 1081, + 1663, + null, + null, + true, + false + ), + ( + 1, + 13, + 1, + 11, + 953, + 1792, + null, + null, + true, + false + ), ( 1, 13, 2, - 4, - 2, + 11, 883, 1862, 7, @@ -123,8 +1074,67 @@ VALUES 1, 13, 2, + 11, + 789, + 1956, + null, + null, + true, + false + ), + ( + 1, + 13, + 1, + 11, + 697, + 2047, + null, + null, + true, + false + ), + ( + 1, + 13, 4, + 11, + 696, + 2048, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 11, + 668, + 2077, + null, + null, + true, + false + ), + ( + 1, + 13, + 2, + 11, + 619, + 2126, + null, + null, + true, + false + ), + ( + 1, + 13, 2, + 11, 613, 2132, 1, @@ -132,12 +1142,23 @@ VALUES true, true ), + ( + 1, + 13, + 31, + 11, + 515, + 2230, + null, + null, + true, + false + ), ( 1, 13, 2, - 4, - 2, + 11, 514, 2231, 31, @@ -148,9 +1169,20 @@ VALUES ( 2, 3, - 14, - 4, + 16, + 11, + 425, + 2320, + null, + null, + true, + false + ), + ( + 2, 3, + 14, + 12, 1193, 2934, 16, @@ -162,8 +1194,7 @@ VALUES 2, 3, 16, - 4, - 3, + 12, 972, 3154, 14, @@ -171,12 +1202,95 @@ VALUES true, true ), + ( + 1, + 12, + 1, + 12, + 892, + 3234, + null, + null, + true, + false + ), + ( + 1, + 12, + 1, + 12, + 868, + 3258, + null, + null, + true, + false + ), ( 2, 3, - 22, - 4, + 14, + 12, + 780, + 3346, + null, + null, + true, + false + ), + ( + 2, + 3, + 21, + 12, + 738, + 3388, + null, + null, + true, + false + ), + ( + 1, + 12, + 2, + 12, + 352, + 3774, + null, + null, + true, + false + ), + ( + 2, + 3, + 16, + 12, + 31, + 4185, + null, + null, + true, + false + ), + ( + 2, + 3, + 14, + 12, + 28, + 4188, + null, + null, + true, + false + ), + ( + 2, 3, + 22, + 12, 16, 4232, 16, diff --git a/src/filters.rs b/src/filters.rs new file mode 100644 index 0000000..3c15bfd --- /dev/null +++ b/src/filters.rs @@ -0,0 +1,5 @@ +pub fn seconds_as_time(secs: &i32) -> ::askama::Result { + let minutes = secs / 60; + let seconds = secs % 60; + Ok(format!("{}:{}", minutes, seconds)) +} diff --git a/src/main.rs b/src/main.rs index 9924a99..a41ab51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,9 @@ mod db; mod model; mod views; +mod filters; use crate::model::{ - TableName, League, Team, Division, @@ -13,14 +13,24 @@ use crate::model::{ Game, }; use views::{ + GoalDetails, + PlayerStats, + TeamStats, + ShotDetails, get_score_from_game, get_box_score_from_game, + get_play_by_play_from_game, + get_goals_from_game, + get_latest_league_for_player, + get_league_player_stats, + get_all_player_stats, }; use sqlx::{ Postgres, Pool, }; +use ormx::Table; use axum::{ Router, http::StatusCode, @@ -38,6 +48,78 @@ use axum::{ use axum_macros::debug_handler; use std::net::SocketAddr; use std::sync::Arc; +use askama::Template; + +#[derive(Template)] +#[template(path="hello.html")] +struct HelloTemplate<'a> { + name: &'a str, + years: i32 +} + +#[derive(Template)] +#[template(path="partials/box_score_table.html")] +struct BoxScoreTemplate { + goals: Vec, +} + +#[derive(Template)] +#[template(path="partials/individual_game_points_table.html")] +struct IndividualGamePointsTableTemplate { + players: Vec, +} + +#[derive(Template)] +#[template(path="partials/team_stats_table.html")] +struct TeamGameStatsTemplate { + teams: Vec, +} + +#[derive(Template)] +#[template(path="division_list.html")] +struct DivisionListTemplate { + league: League, + divisions: Vec, +} + +#[derive(Template)] +#[template(path="league_list.html")] +struct LeagueListTemplate { + leagues: Vec, +} + +#[derive(Template)] +#[template(path="game_list.html")] +struct GameListTemplate { + division: Division, + games: Vec, +} + +#[derive(Template)] +#[template(path="partials/play_by_play_table.html")] +struct ShotsTableTemplate { + shots: Vec, +} + +#[derive(Template)] +#[template(path="game_score_page.html")] +struct GameScorePageTemplate { + game: Game, + division: Division, + box_score: BoxScoreTemplate, + team_stats: TeamGameStatsTemplate, + individual_stats: IndividualGamePointsTableTemplate, + play_by_play: ShotsTableTemplate, +} + +#[derive(Template)] +#[template(path="player_page.html")] +pub struct PlayerPageTemplate { + player: Player, + league: League, + league_stats: PlayerStats, + lifetime_stats: PlayerStats, +} #[derive(Clone)] pub struct ServerState { @@ -52,9 +134,12 @@ async fn main() { }; let router = Router::new() .route("/", get(league_html)) - .route("/league/:id/divisions/", get(divisions_for_league_html)) + .route("/shots/", get(shots_all)) + .route("/test/", get(test_template)) + .route("/league/:id/", get(divisions_for_league_html)) .route("/division/:id/", get(games_for_division_html)) .route("/game/:id/", get(score_for_game_html)) + .route("/player/:name/", get(player_from_name)) .with_state(state); let addr = SocketAddr::from(([127, 0, 0, 1], 8000)); println!("Listening on {}", addr); @@ -64,39 +149,38 @@ async fn main() { .unwrap(); } -async fn get_all sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool) -> Result, sqlx::Error> { - sqlx::query_as::<_, T>( - &format!("SELECT * FROM {};", ::TABLE_NAME) - ) - .fetch_all(pool) - .await -} -async fn get_by_id sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool, id: i32) -> Result, sqlx::Error> { - sqlx::query_as::<_, T>( - &format!("SELECT * FROM {} WHERE id = $1;", ::TABLE_NAME) - ) - .bind(id) - .fetch_optional(pool) - .await +async fn player_from_name(State(server_config): State, Path(name): Path) -> impl IntoResponse { + let player = Player::from_name_case_insensitive(&server_config.db_pool, name) + .await + .unwrap(); + let latest_league = get_latest_league_for_player(&server_config.db_pool, &player) + .await + .unwrap() + .unwrap(); + let latest_league_stats = get_league_player_stats(&server_config.db_pool, &player, &latest_league) + .await + .unwrap(); + let lifetime_stats = get_all_player_stats(&server_config.db_pool, &player) + .await + .unwrap(); + let html = PlayerPageTemplate { + player, + league: latest_league, + league_stats: latest_league_stats, + lifetime_stats, + }; + (StatusCode::OK, html) } -/* -async fn insert_into sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool, new: &T) -> Result { - let query = sql_builder::SqlBuilder::insert_into(::TABLE_NAME) - .values(()) - .sql().unwrap(); - sqlx::query( - &query - ) - .execute(pool) - .await + +async fn test_template<'a>() -> HelloTemplate<'a> { + HelloTemplate { name: "Tait", years: 24 } } -*/ macro_rules! get_all { ($crud_struct:ident, $func_name:ident) => { #[debug_handler] async fn $func_name(State(server_config): State) -> impl IntoResponse { - let cruder = get_all::<$crud_struct>(&server_config.db_pool) + let cruder = $crud_struct::all(&*server_config.db_pool) .await .unwrap(); (StatusCode::OK, Json(cruder)) @@ -107,7 +191,7 @@ macro_rules! get_by_id { ($crud_struct:ident, $func_name:ident) => { #[debug_handler] async fn $func_name(State(server_config): State, Path(id): Path) -> impl IntoResponse { - let cruder = get_by_id::<$crud_struct>(&server_config.db_pool, id) + let cruder = $crud_struct::get(&*server_config.db_pool, id) .await .unwrap(); (StatusCode::OK, Json(cruder)) @@ -116,60 +200,41 @@ macro_rules! get_by_id { } async fn league_html(State(server_config): State) -> impl IntoResponse { - let leagues_html = get_all::(&server_config.db_pool).await - .unwrap() - .iter() - .map(|league| { - format!( - "
  • {0}
  • ", - league.name, - format!("/league/{}/divisions/", league.id), - ) - }) - .collect::>() - .join("\n"); - let html = format!("
      {leagues_html}
    "); - (StatusCode::OK, Html(html)) + let leagues = League::all(&*server_config.db_pool) + .await + .unwrap(); + let leagues_template = LeagueListTemplate { + leagues + }; + (StatusCode::OK, leagues_template) } async fn divisions_for_league_html(State(server_config): State, Path(league_id): Path) -> impl IntoResponse { - let leagues_html = sqlx::query_as::<_, Division>("SELECT * FROM divisions WHERE league = $1") - .bind(league_id) - .fetch_all(&*server_config.db_pool) + let league = League::get(&*server_config.db_pool, league_id) .await - .unwrap() - .iter() - .map(|division| { - format!( - "
  • {0}
  • ", - division.name, - format!("/division/{}/", division.id), - ) - }) - .collect::>() - .join("\n"); - let html = format!("
      {leagues_html}
    "); - (StatusCode::OK, Html(html)) + .unwrap(); + let divisions = Division::by_league(&*server_config.db_pool, league_id) + .await + .unwrap(); + let html = DivisionListTemplate { + league, + divisions + }; + (StatusCode::OK, html) } async fn games_for_division_html(State(server_config): State, Path(division_id): Path) -> impl IntoResponse { - let leagues_html = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE division = $1") - .bind(division_id) - .fetch_all(&*server_config.db_pool) + let division = Division::get(&*server_config.db_pool, division_id) .await - .unwrap() - .iter() - .map(|game| { - format!( - "
  • {0}
  • ", - game.name, - format!("/game/{}/", game.id), - ) - }) - .collect::>() - .join("\n"); - let html = format!("
      {leagues_html}
    "); - (StatusCode::OK, Html(html)) + .unwrap(); + let games = Game::by_division(&*server_config.db_pool, division.id) + .await + .unwrap(); + let games_template = GameListTemplate { + division, + games + }; + (StatusCode::OK, games_template) } async fn score_for_game_html(State(server_config): State, Path(game_id): Path) -> impl IntoResponse { let game = sqlx::query_as::<_, Game>( @@ -179,16 +244,28 @@ async fn score_for_game_html(State(server_config): State, Path(game .fetch_one(&*server_config.db_pool) .await .unwrap(); + let division = Division::get(&*server_config.db_pool, game.division) + .await + .unwrap(); + let pbp = get_play_by_play_from_game(&server_config.db_pool, &game).await.unwrap(); let score = get_score_from_game(&server_config.db_pool, &game).await.unwrap(); - let box_score_html = get_box_score_from_game(&server_config.db_pool, &game).await.unwrap() - .iter() - .map(|player_stats| { - format!("{0}{1}{2}{3}", player_stats.player_name, player_stats.points, player_stats.goals, player_stats.assists) - }) - .collect::>() - .join(""); - let html = format!("

    {}: {}
    {}: {}

    {}
    ", score.home_name, score.home, score.away_name, score.away, box_score_html); - (StatusCode::OK, Html(html)) + let score_html = TeamGameStatsTemplate { teams: score }; + let goal_details = get_box_score_from_game(&server_config.db_pool, &game).await.unwrap(); + let goal_details_html = IndividualGamePointsTableTemplate { players: goal_details }; + let box_score = get_goals_from_game(&server_config.db_pool, &game).await.unwrap(); + let box_score_html = BoxScoreTemplate { goals: box_score }; + let pbp_html = ShotsTableTemplate { + shots: pbp + }; + let game_template = GameScorePageTemplate { + division, + game, + box_score: box_score_html, + team_stats: score_html, + individual_stats: goal_details_html, + play_by_play: pbp_html, + }; + (StatusCode::OK, game_template) } /* @@ -203,6 +280,7 @@ macro_rules! insert { } } } +*/ macro_rules! impl_all_query_types { ($ty:ident, $func_all:ident, $func_by_id:ident) => { @@ -241,5 +319,3 @@ impl_all_query_types!( league_all, league_id ); - -*/ diff --git a/src/model.rs b/src/model.rs index a981955..6782c2d 100644 --- a/src/model.rs +++ b/src/model.rs @@ -7,15 +7,17 @@ pub trait TableName { const TABLE_NAME: &'static str; } macro_rules! impl_table_name { - ($ty:ident, $tname:expr) => { + ($ty:ident, $tname:literal) => { impl TableName for $ty { const TABLE_NAME: &'static str = $tname; } } } -#[derive(FromRow, Serialize, Deserialize, Debug)] +#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)] +#[ormx(table = "leagues", id = id, insertable, deletable)] pub struct League { + #[ormx(default)] pub id: i32, pub name: String, #[serde(with = "ts_seconds")] @@ -23,7 +25,8 @@ pub struct League { #[serde(with = "ts_seconds_option")] pub end_date: Option>, } -#[derive(FromRow, Serialize, Deserialize)] +#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)] +#[ormx(table_name = "leagues", table = League, id = "id")] pub struct NewLeague { pub name: String, #[serde(with = "ts_seconds")] @@ -32,72 +35,104 @@ pub struct NewLeague { pub end_date: Option>, } -#[derive(FromRow, Serialize, Deserialize)] +#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)] +#[ormx(table = "divisions", id = id, insertable, deletable)] pub struct Division { + #[ormx(default)] pub id: i32, pub name: String, + #[ormx(get_many(i32))] pub league: i32, } -#[derive(FromRow, Serialize, Deserialize)] +#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)] +#[ormx(table_name = "divisions", table = Division, id = "id")] pub struct NewDivision { - pub id: i32, pub name: String, pub league: i32, } -#[derive(FromRow, Serialize, Deserialize)] +#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)] +#[ormx(table = "teams", id = id, insertable, deletable)] pub struct Team { + #[ormx(default)] pub id: i32, pub name: String, pub division: i32, pub image: Option, } -#[derive(FromRow, Serialize, Deserialize)] +#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Patch)] +#[ormx(table_name = "teams", table = Team, id = "id")] pub struct NewTeam { pub name: String, pub division: i32, } -#[derive(FromRow, Serialize, Deserialize)] +#[derive(FromRow, Serialize, Deserialize, Debug, ormx::Table)] +#[ormx(table = "players", id = id, insertable, deletable)] pub struct Player { + #[ormx(default)] pub id: i32, pub name: String, pub weight_kg: Option, pub height_cm: Option, } -#[derive(FromRow, Deserialize, Serialize)] +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);") + .bind(name) + .fetch_optional(pool) + .await + .unwrap() + } +} + +#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Patch)] +#[ormx(table_name = "players", table = Player, id = "id")] pub struct NewPlayer { pub name: String, pub weight_kg: Option, pub height_cm: Option, } -#[derive(FromRow, Deserialize, Serialize)] +#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)] +#[ormx(table = "shots", id = id, insertable, deletable)] pub struct Shot { + #[ormx(default)] pub id: i32, pub shooter_team: i32, + pub shooter: i32, pub goalie: i32, pub assistant: Option, - pub game: i32, pub period: i32, pub period_time: i32, pub video_timestamp: Option, + pub blocker: Option, + pub on_net: bool, + pub assistant_second: Option, + pub goal: bool, + #[serde(with = "ts_seconds")] + pub created_at: DateTime, } -#[derive(FromRow, Deserialize, Serialize)] +#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)] +#[ormx(table = "team_players", id = id, insertable, deletable)] pub struct TeamPlayer { + #[ormx(default)] pub id: i32, pub team: i32, pub player: i32, pub position: i32, } -#[derive(FromRow, Deserialize, Serialize)] +#[derive(FromRow, Deserialize, Serialize, Debug, ormx::Table)] +#[ormx(table = "games", id = id, insertable, deletable)] pub struct Game { + #[ormx(default)] pub id: i32, + #[ormx(get_many(i32))] pub division: i32, pub name: String, pub team_home: i32, @@ -126,6 +161,17 @@ mod tests { Game, }; + #[test] + fn test_get_player_from_name() { + tokio_test::block_on(async move { + let pool = db_connect().await; + let player = Player::from_name_case_insensitive(&pool, "tait-hoyem".to_string()).await; + assert!(player.is_some()); + let player = player.unwrap(); + assert_eq!(player.name, "Tait Hoyem"); + }) + } + /// A simple function to connect to the database. async fn db_connect() -> sqlx::PgPool { let db_url = env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set to run tests."); diff --git a/src/views.rs b/src/views.rs index 161c020..deb5b09 100644 --- a/src/views.rs +++ b/src/views.rs @@ -1,14 +1,17 @@ use sqlx::FromRow; use sqlx::PgPool; -use crate::model::Game; +use crate::model::{ + Player, + Game, + League, +}; use serde::{Serialize, Deserialize}; #[derive(FromRow, Deserialize, Serialize, Debug)] -pub struct Score { - pub home: i64, - pub home_name: String, - pub away: i64, - pub away_name: String, +pub struct TeamStats { + pub name: String, + pub goals: i64, + pub shots: i64, } #[derive(FromRow, Deserialize, Serialize, Debug)] @@ -23,58 +26,244 @@ pub struct Notification { #[derive(FromRow, Deserialize, Serialize, Debug)] pub struct PlayerStats { - pub player_name: String, + pub name: String, pub goals: i64, pub assists: i64, pub points: i64, } - pub async fn get_box_score_from_game(pool: &PgPool, game: &Game) -> Result, sqlx::Error> { - let query = format!(r#" + let query = r#" SELECT ( - SELECT COUNT(id) + SELECT COUNT(shots.id) FROM shots + JOIN periods ON periods.id=shots.period WHERE shooter=players.id AND goal=true - AND game=$1 + AND periods.game=$1 ) AS goals, ( - SELECT COUNT(id) + SELECT COUNT(shots.id) FROM shots - WHERE assistant=players.id - AND goal=true - AND game=$1 + JOIN periods ON periods.id=shots.period + WHERE (assistant=players.id + OR assistant_second=players.id) + AND goal=true + AND periods.game=$1 ) AS assists, ( - SELECT COUNT(id) + SELECT COUNT(shots.id) FROM shots + JOIN periods ON periods.id=shots.period WHERE (assistant=players.id + OR assistant_second=players.id OR shooter=players.id) - AND game=$1 + AND goal=true + AND periods.game=$1 ) AS points, - players.name AS player_name + players.name AS name FROM players JOIN shots ON shots.shooter=players.id OR shots.assistant=players.id -WHERE shots.game = $1 +JOIN periods ON periods.id=shots.period +WHERE periods.game = $1 GROUP BY players.id +-- exclude players who do not have any points +-- NOTE: we can NOT use the aliased column "points" here, so we need to recalculate it. +-- This should not be a performance problem because the optimizer should deal with duplicate sub-queries. +HAVING + ( + SELECT COUNT(shots.id) + FROM shots + JOIN periods ON periods.id=shots.period + WHERE (assistant=players.id + OR assistant_second=players.id + OR shooter=players.id) + AND goal=true + AND periods.game=$1 + ) > 0 ORDER BY points DESC, goals DESC, players.name; -"#); +"#; sqlx::query_as::<_, PlayerStats>(&query) .bind(game.id) .fetch_all(pool) .await } +pub async fn get_latest_league_for_player(pool: &PgPool, player: &Player) -> Result, sqlx::Error> { + let query = +r#" +SELECT leagues.* +FROM players +JOIN team_players ON team_players.player=players.id +JOIN teams ON teams.id=team_players.team +JOIN divisions ON divisions.id=teams.division +JOIN leagues ON leagues.id=divisions.league +WHERE players.id=$1 +ORDER BY leagues.end_date DESC +LIMIT 1; +"#; + sqlx::query_as::<_, League>(&query) + .bind(player.id) + .fetch_optional(pool) + .await +} + +pub async fn get_league_player_stats(pool: &PgPool, player: &Player, league: &League) -> Result { + let query = r#" +SELECT + ( + SELECT COUNT(shots.id) + FROM shots + JOIN periods ON periods.id=shots.period + JOIN games ON games.id=periods.game + JOIN divisions ON divisions.id=games.division + JOIN leagues ON leagues.id=divisions.league + WHERE shots.goal=true + AND shots.shooter=players.id + AND leagues.id=$2 + ) AS goals, + ( + SELECT COUNT(shots.id) + FROM shots + JOIN periods ON periods.id=shots.period + JOIN games ON games.id=periods.game + JOIN divisions ON divisions.id=games.division + JOIN leagues ON leagues.id=divisions.league + WHERE shots.goal=true + AND leagues.id=$2 + AND (shots.assistant=players.id + OR shots.assistant_second=players.id) + ) AS assists, + ( + SELECT COUNT(shots.id) + FROM shots + JOIN periods ON periods.id=shots.period + JOIN games ON games.id=periods.game + JOIN divisions ON divisions.id=games.division + JOIN leagues ON leagues.id=divisions.league + WHERE shots.goal=true + AND leagues.id=$2 + AND (shots.shooter=players.id + OR shots.assistant=players.id + OR shots.assistant_second=players.id) + ) AS points, + players.name AS name +FROM players +WHERE id=$1; +"#; + sqlx::query_as::<_, PlayerStats>(&query) + .bind(player.id) + .bind(league.id) + .fetch_one(pool) + .await +} + +pub async fn get_latest_stats(pool: &PgPool, player: &Player) -> Result, sqlx::Error> { + let query = r#" +SELECT + shots.shooter AS player_id, + shots.assistant AS first_assist_id, + shots.assistant_second AS second_assist_id, + ( + SELECT name + FROM players + WHERE id=shots.shooter + ) AS player_name, + ( + SELECT name + FROM players + WHERE id=shots.assistant + ) AS first_assist_name, + ( + SELECT name + FROM players + WHERE id=shots.assistant_second + ) AS second_assist_name, + ( + SELECT player_number + FROM team_players + WHERE player=shots.shooter + AND team=shots.shooter_team + ) AS player_number, + ( + SELECT player_number + FROM team_players + WHERE player=shots.assistant + AND team=shots.shooter_team + ) AS first_assist_number, + ( + SELECT player_number + FROM team_players + WHERE player=shots.assistant_second + AND team=shots.shooter_team + ) AS second_assist_number, + teams.name AS team_name, + teams.id AS team_id, + shots.shooter_team AS player_team, + shots.period_time AS time_remaining, + period_types.id AS period_id, + period_types.short_name AS period_short_name +FROM shots +JOIN periods ON shots.period=periods.id +JOIN period_types ON periods.period_type=period_types.id +JOIN teams ON shots.shooter_team=teams.id +WHERE shots.shooter=$1 +ORDER BY + shots.created_at DESC, + periods.period_type DESC, + shots.period_time ASC +LIMIT 5; +"#; + let x =sqlx::query_as::<_, GoalDetails>(&query) + .bind(player.id); + //.fetch_all(pool) + //.await + x.fetch_all(pool).await +} + +pub async fn get_all_player_stats(pool: &PgPool, player: &Player) -> Result { + let query =r#" +SELECT + ( + SELECT COUNT(id) + FROM shots + WHERE shots.goal=true + AND shots.shooter=players.id + ) AS goals, + ( + SELECT COUNT(id) + FROM shots + WHERE shots.goal=true + AND (shots.assistant=players.id + OR shots.assistant_second=players.id) + ) AS assists, + ( + SELECT COUNT(id) + FROM shots + WHERE shots.goal=true + AND (shots.shooter=players.id + OR shots.assistant=players.id + OR shots.assistant_second=players.id) + ) AS points, + players.name AS name +FROM players +WHERE id=$1; +"#; + sqlx::query_as::<_, PlayerStats>(&query) + .bind(player.id) + .fetch_one(pool) + .await +} + #[derive(FromRow, Deserialize, Serialize, Debug)] -pub struct DetailGoals { +pub struct GoalDetails { pub player_id: i32, pub player_name: String, - pub number: i32, + pub player_number: i32, pub team_name: String, pub team_id: i32, pub time_remaining: i32, @@ -87,43 +276,172 @@ pub struct DetailGoals { pub second_assist_number: Option, } -pub async fn get_goals_from_game(pool: &PgPool, game: &Game) -> Result, sqlx::Error> { +#[derive(FromRow, Deserialize, Serialize, Debug)] +pub struct ShotDetails { + pub player_id: i32, + pub player_name: String, + pub player_number: i32, + pub team_name: String, + pub team_id: i32, + pub is_goal: bool, + pub time_remaining: i32, + pub period_short_name: String, + pub first_assist_name: Option, + pub first_assist_number: Option, + pub first_assist_id: Option, + pub second_assist_name: Option, + pub second_assist_id: Option, + pub second_assist_number: Option, } -pub async fn get_score_from_game(pool: &PgPool, game: &Game) -> Result { - let query = format!(r#" -SELECT +pub async fn get_goals_from_game(pool: &PgPool, game: &Game) -> 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, ( - SELECT COUNT(id) - FROM shots - WHERE game=$1 - AND goal=true - AND shooter_team=$2 - ) AS home, + SELECT name + FROM players + WHERE id=shots.shooter + ) AS player_name, ( - SELECT COUNT(id) - FROM shots - WHERE game=$1 - AND goal=true - AND shooter_team=$3 - ) AS away, + SELECT name + FROM players + WHERE id=shots.assistant + ) AS first_assist_name, + ( + SELECT name + FROM players + WHERE id=shots.assistant_second + ) AS second_assist_name, + ( + SELECT player_number + FROM team_players + WHERE player=shots.shooter + AND team=shots.shooter_team + ) AS player_number, + ( + SELECT player_number + FROM team_players + WHERE player=shots.assistant + AND team=shots.shooter_team + ) AS first_assist_number, + ( + SELECT player_number + FROM team_players + WHERE player=shots.assistant_second + AND team=shots.shooter_team + ) AS second_assist_number, + teams.name AS team_name, + teams.id AS team_id, + shots.shooter_team AS player_team, + shots.period_time AS time_remaining, + period_types.id AS period_id, + period_types.short_name AS period_short_name +FROM shots +JOIN periods ON shots.period=periods.id +JOIN period_types ON periods.period_type=period_types.id +JOIN teams ON shots.shooter_team=teams.id +WHERE shots.goal=true + AND periods.game=$1 +ORDER BY + periods.period_type ASC, + shots.period_time DESC; +"#) + .bind(game.id) + .fetch_all(pool) + .await +} + +pub async fn get_play_by_play_from_game(pool: &PgPool, game: &Game) -> Result, sqlx::Error> { + sqlx::query_as::<_, ShotDetails>( +r#" +SELECT + shots.shooter AS player_id, + shots.assistant AS first_assist_id, + shots.assistant_second AS second_assist_id, + shots.goal AS is_goal, + ( + SELECT name + FROM players + WHERE id=shots.shooter + ) AS player_name, ( SELECT name - FROM teams - WHERE id=$2 - ) AS home_name, + FROM players + WHERE id=shots.assistant + ) AS first_assist_name, ( SELECT name - FROM teams - WHERE id=$3 - ) AS away_name -FROM games; + FROM players + WHERE id=shots.assistant_second + ) AS second_assist_name, + ( + SELECT player_number + FROM team_players + WHERE player=shots.shooter + AND team=shots.shooter_team + ) AS player_number, + ( + SELECT player_number + FROM team_players + WHERE player=shots.assistant + AND team=shots.shooter_team + ) AS first_assist_number, + ( + SELECT player_number + FROM team_players + WHERE player=shots.assistant_second + AND team=shots.shooter_team + ) AS second_assist_number, + teams.name AS team_name, + teams.id AS team_id, + shots.shooter_team AS player_team, + shots.period_time AS time_remaining, + period_types.id AS period_id, + period_types.short_name AS period_short_name +FROM shots +JOIN periods ON shots.period=periods.id +JOIN period_types ON periods.period_type=period_types.id +JOIN teams ON shots.shooter_team=teams.id +WHERE periods.game=$1 +ORDER BY + periods.period_type ASC, + shots.period_time DESC; +"#) + .bind(game.id) + .fetch_all(pool) + .await +} + +pub async fn get_score_from_game(pool: &PgPool, game: &Game) -> Result, sqlx::Error> { + let query = format!(r#" +SELECT + ( + SELECT COUNT(shots.id) + FROM shots + JOIN periods ON periods.id=shots.period + WHERE periods.game=$1 + AND shots.goal=true + AND shots.shooter_team=teams.id + ) AS goals, + ( + SELECT COUNT(shots.id) + FROM shots + JOIN periods ON periods.id=shots.period + WHERE periods.game=$1 + AND shooter_team=teams.id + ) AS shots, + teams.name AS name +FROM games +JOIN teams ON teams.id=games.team_home OR teams.id=games.team_away +WHERE games.id=$1; "#); - sqlx::query_as::<_, Score>(&query) + sqlx::query_as::<_, TeamStats>(&query) .bind(game.id) - .bind(game.team_home) - .bind(game.team_away) - .fetch_one(pool) + .fetch_all(pool) .await } @@ -139,7 +457,7 @@ SELECT ( SELECT COUNT(id) FROM shots - WHERE assistant=players.id + WHERE (assistant=players.id OR assistant_second=players.id) AND goal=true ) AS assists, ( @@ -148,7 +466,7 @@ SELECT WHERE assistant=players.id OR shooter=players.id ) AS points, - players.name AS player_name + players.name AS name FROM players ORDER BY points DESC, @@ -166,19 +484,101 @@ mod tests { use std::env; use crate::model::{ Game, + Player, + League, }; use crate::views::{ Notification, get_player_stats_overview, get_score_from_game, + get_goals_from_game, get_box_score_from_game, + get_latest_league_for_player, + get_league_player_stats, + get_all_player_stats, + get_latest_stats, }; + #[test] + fn get_latest_stats_of_player() { + tokio_test::block_on(async move { + let pool = db_connect().await; + let player = sqlx::query_as::<_, Player>("SELECT * FROM id=2;") + .fetch_one(&pool) + .await + .unwrap(); + let latest = get_latest_stats(&pool, &player) + .await + .unwrap(); + }) + } + + #[test] + fn check_all_player_stats() { + tokio_test::block_on(async move { + let pool = db_connect().await; + let player = sqlx::query_as::<_, Player>("SELECT * FROM players WHERE id=2;") + .fetch_one(&pool) + .await + .unwrap(); + let stats = get_all_player_stats(&pool, &player).await.unwrap(); + assert_eq!(stats.name, "Hillary Scanlon"); + }) + } + + #[test] + fn check_league_player_stats() { + tokio_test::block_on(async move { + let pool = db_connect().await; + let league = sqlx::query_as::<_, League>("SELECT * FROM leagues WHERE id=1;") + .fetch_one(&pool) + .await + .unwrap(); + let player = sqlx::query_as::<_, Player>("SELECT * FROM players WHERE id=2;") + .fetch_one(&pool) + .await + .unwrap(); + let stats = get_league_player_stats(&pool, &player, &league).await.unwrap(); + assert_eq!(stats.name, "Hillary Scanlon"); + }) + } + + #[test] + fn check_latest_league_for_player() { + tokio_test::block_on(async move { + let pool = db_connect().await; + let player = sqlx::query_as::<_, Player>("SELECT * FROM players WHERE id=5") + .fetch_one(&pool) + .await + .unwrap(); + let league = get_latest_league_for_player(&pool, &player) + .await + .unwrap() + .unwrap(); + assert_eq!(league.id, 1); + }) + } + + #[test] + fn check_score_details_from_game() { + tokio_test::block_on(async move { + let pool = db_connect().await; + let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id=1;") + .fetch_one(&pool) + .await + .unwrap(); + let scores = get_goals_from_game(&pool, &game) + .await + .unwrap(); + println!("{scores:?}"); + }) + } + #[test] fn check_box_score_from_game() { tokio_test::block_on(async move{ let pool = db_connect().await; - let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id=1;") + let game = sqlx::query_as::<_, Game>("SELECT * FROM games WHERE id=4;") .fetch_one(&pool) .await .unwrap(); @@ -186,7 +586,12 @@ mod tests { .await .unwrap(); println!("{scores:?}"); - assert_eq!(scores.get(0).unwrap().player_name, "Brian MacLean"); + let second_top_scorer = scores.get(1).unwrap(); + assert_eq!(second_top_scorer.name, "Allyssa Foulds"); + assert_eq!(second_top_scorer.goals, 1, "Allysa should have 1 goal.."); + assert_eq!(second_top_scorer.assists, 2, "Allyssa should have 2 assists."); + assert_eq!(second_top_scorer.points, 3, "Allysa should have 3 points."); + assert_eq!(scores.len(), 8, "Players which did not receive any points should not be in the box score."); }) } @@ -201,8 +606,8 @@ mod tests { let score = get_score_from_game(&pool, &game) .await .unwrap(); - assert_eq!(score.away, 1); - assert_eq!(score.home, 1); + assert_eq!(score.get(0).unwrap().goals, 1); + assert_eq!(score.get(1).unwrap().goals, 1); }) } @@ -228,13 +633,14 @@ SELECT positions.name AS position, team_players.player_number AS scorer_number, shots.period_time AS period_time_left, - periods.name AS period_name + period_types.name AS period_name FROM shots JOIN teams ON teams.id=shots.shooter_team JOIN players ON players.id=shots.shooter JOIN team_players ON team_players.player=players.id AND team_players.team=teams.id JOIN periods ON periods.id=shots.period +JOIN period_types ON period_types.id=periods.period_type JOIN positions ON positions.id=team_players.position; "#; let result = sqlx::query_as::<_, Notification>(query) diff --git a/templates/division_list.html b/templates/division_list.html new file mode 100644 index 0000000..143fa8e --- /dev/null +++ b/templates/division_list.html @@ -0,0 +1,6 @@ +

    Divisions for the {{ league.name }}

    + diff --git a/templates/game_list.html b/templates/game_list.html new file mode 100644 index 0000000..77c4cda --- /dev/null +++ b/templates/game_list.html @@ -0,0 +1,10 @@ +

    Games for {{ division.name }}

    +{% if games.len() > 0 %} +
      + {% for game in games %} +
    1. {{ game.name }}
    2. + {% endfor %} +
    +{% else %} +

    No games have been recorded.

    +{% endif %} diff --git a/templates/game_score_page.html b/templates/game_score_page.html new file mode 100644 index 0000000..d8dee08 --- /dev/null +++ b/templates/game_score_page.html @@ -0,0 +1,9 @@ +

    {{ game.name }} of the {{ division.name }}

    +

    Team

    +{{ team_stats|safe }} +

    Individual

    +{{ individual_stats|safe }} +

    Box Score

    +{{ box_score|safe }} +

    Play-by-Play

    +{{ play_by_play|safe }} diff --git a/templates/hello.html b/templates/hello.html new file mode 100644 index 0000000..3fbe3bb --- /dev/null +++ b/templates/hello.html @@ -0,0 +1,2 @@ +Hello, World! +My name is {{ name }} and I am {{ years }} years old. diff --git a/templates/league_list.html b/templates/league_list.html new file mode 100644 index 0000000..f1cfc45 --- /dev/null +++ b/templates/league_list.html @@ -0,0 +1,6 @@ +

    IBIHF Leagues

    +
      + {% for league in leagues %} +
    1. {{ league.name }}
    2. + {% endfor %} +
    diff --git a/templates/partials/box_score_table.html b/templates/partials/box_score_table.html new file mode 100644 index 0000000..03db84c --- /dev/null +++ b/templates/partials/box_score_table.html @@ -0,0 +1,36 @@ + + + + + + + + + + + {% for goal in goals %} + + + + + + + + + + {% endfor %} + +
    Scorer + Team#PeriodTimeAssistSecondary Assist
    {{ 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 %} + unassisted + {% endif %} + + {% if goal.second_assist_name.is_some() %} + {{ goal.second_assist_name.as_ref().unwrap() }} + {% else %} + N/A + {% endif %} +
    diff --git a/templates/partials/individual_game_points_table.html b/templates/partials/individual_game_points_table.html new file mode 100644 index 0000000..9b3680c --- /dev/null +++ b/templates/partials/individual_game_points_table.html @@ -0,0 +1,20 @@ + + + + + + + + + + + {% for player in players %} + + + + + + + {% endfor %} + +
    NamePointsGoalsAssists
    {{ player.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 new file mode 100644 index 0000000..d76e707 --- /dev/null +++ b/templates/partials/play_by_play_table.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + {% for shot in shots %} + + + + + + + + + + + {% endfor %} + +
    Shooter + Team#TypePeriodTimeAssistSecondary Assist
    {{ shot.player_name }}{{ shot.team_name }}{{ shot.player_number }} + {% if shot.is_goal %} + Goal + {% else %} + Shot + {% endif %} + {{ 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 %} + unassisted + {% endif %} + {% else %} + N/A + {% endif %} + + {% if shot.second_assist_name.is_some() %} + {{ shot.second_assist_name.as_ref().unwrap() }} + {% else %} + N/A + {% endif %} +
    diff --git a/templates/partials/team_stats_table.html b/templates/partials/team_stats_table.html new file mode 100644 index 0000000..8bf6512 --- /dev/null +++ b/templates/partials/team_stats_table.html @@ -0,0 +1,18 @@ + + + + + + + + + + {% for team in teams %} + + + + + + {% endfor %} + +
    TeamGoalsShots
    {{ team.name }}{{ team.goals }}{{ team.shots }}
    diff --git a/templates/player_page.html b/templates/player_page.html new file mode 100644 index 0000000..e9f0e81 --- /dev/null +++ b/templates/player_page.html @@ -0,0 +1,17 @@ + +

    {{ player.name }}

    +

    Latest Competition: {{ league.name }}

    + +{{ league_stats.points }} + +{{ league_stats.goals }} + +{{ league_stats.goals }} +

    Lifetime Stats

    + +{{ lifetime_stats.points }} + +{{ lifetime_stats.goals }} + +{{ lifetime_stats.goals }} +