From e3b2d57331310d47eb0520cbd14678aca8261e96 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 15:57:31 -0400 Subject: [PATCH 01/19] (migrations) modifications to old tables & functions --- .../20250722002114_unrated_levels.down.sql | 198 +++++++++++++ .../20250722002114_unrated_levels.up.sql | 260 ++++++++++++++++++ 2 files changed, 458 insertions(+) create mode 100644 migrations/20250722002114_unrated_levels.down.sql create mode 100644 migrations/20250722002114_unrated_levels.up.sql diff --git a/migrations/20250722002114_unrated_levels.down.sql b/migrations/20250722002114_unrated_levels.down.sql new file mode 100644 index 00000000..fa66d2bf --- /dev/null +++ b/migrations/20250722002114_unrated_levels.down.sql @@ -0,0 +1,198 @@ +DROP VIEW score_giving; + +CREATE VIEW score_giving AS + SELECT records.progress, demons.position, demons.requirement, records.player + FROM records + INNER JOIN demons + ON demons.id = records.demon + WHERE records.status_ = 'APPROVED' AND (demons.position <= 75 OR records.progress = 100) + + UNION + + SELECT 100, demons.position, demons.requirement, demons.verifier + FROM demons; + +DROP FUNCTION score_of_player(BOOLEAN, INTEGER); +CREATE OR REPLACE FUNCTION score_of_player(player_id INTEGER) RETURNS DOUBLE PRECISION AS $$ + SELECT SUM(record_score(progress, position, 150, requirement)) + FROM score_giving + WHERE player = player_id +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION recompute_player_scores() RETURNS void AS $$ + UPDATE players + SET score = coalesce(q.score, 0) + FROM players p + LEFT OUTER JOIN ( + SELECT player, SUM(record_score(progress, position, 150, requirement)) as score + FROM score_giving + GROUP BY player + ) q + ON q.player = p.id + WHERE players.id = p.id; +$$ LANGUAGE SQL; + +DROP FUNCTION score_of_nation(BOOLEAN, VARCHAR(2)); +CREATE OR REPLACE FUNCTION score_of_nation(iso_country_code VARCHAR(2)) RETURNS DOUBLE PRECISION AS $$ + SELECT SUM(record_score(q.progress, q.position, 150, q.requirement)) + FROM ( + SELECT DISTINCT ON (position) * from score_giving + INNER JOIN players + ON players.id=player + WHERE players.nationality = iso_country_code + ORDER BY position, progress DESC + ) q +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION recompute_nation_scores() RETURNS void AS $$ + UPDATE nationalities + SET score = COALESCE(p.sum, 0) + FROM nationalities n + LEFT OUTER JOIN ( + SELECT nationality, SUM(record_score(q.progress, q.position, 150, q.requirement)) + FROM ( + SELECT DISTINCT ON (position, nationality) * from score_giving + INNER JOIN players + ON players.id=player + WHERE players.nationality IS NOT NULL + ORDER BY players.nationality, position, progress DESC + ) q + GROUP BY nationality + ) p + ON p.nationality = n.iso_country_code + WHERE n.iso_country_code = nationalities.iso_country_code +$$ LANGUAGE SQL; + +DROP FUNCTION score_of_subdivision(BOOLEAN, VARCHAR(2), VARCHAR(3)); +CREATE OR REPLACE FUNCTION score_of_subdivision(iso_country_code VARCHAR(2), iso_code VARCHAR(3)) RETURNS DOUBLE PRECISION AS $$ + SELECT SUM(record_score(q.progress, q.position, 150, q.requirement)) + FROM ( + SELECT DISTINCT ON (position) * from score_giving + INNER JOIN players + ON players.id=player + WHERE players.nationality = iso_country_code + AND players.subdivision = iso_code + ORDER BY position, progress DESC + ) q +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION recompute_subdivision_scores() RETURNS void AS $$ + UPDATE subdivisions + SET score = COALESCE(p.sum, 0) + FROM subdivisions s + LEFT OUTER JOIN ( + SELECT nationality, subdivision, SUM(record_score(q.progress, q.position, 150, q.requirement)) + FROM ( + SELECT DISTINCT ON (position, nationality, subdivision) * from score_giving + INNER JOIN players + ON players.id=player + WHERE players.nationality IS NOT NULL + AND players.subdivision IS NOT NULL + ORDER BY players.nationality, players.subdivision, position, progress DESC + ) q + GROUP BY nationality, subdivision + ) p + ON s.nation = p.nationality AND s.iso_code = p.subdivision + WHERE s.nation = subdivisions.nation + AND s.iso_code = subdivisions.iso_code +$$ LANGUAGE SQL; + +DROP VIEW ranked_players; +DROP MATERIALIZED VIEW player_ranks; + +CREATE MATERIALIZED VIEW player_ranks AS + SELECT + RANK() OVER (ORDER BY score DESC) as rank, + id + FROM players + WHERE + score != 0 AND NOT banned; + +CREATE UNIQUE INDEX player_ranks_id_idx ON player_ranks(id); + +CREATE VIEW ranked_players AS +SELECT + ROW_NUMBER() OVER(ORDER BY rank, id) AS index, + rank, + id, name, players.score, subdivision, + nationalities.iso_country_code, + nationalities.nation, + nationalities.continent +FROM players +LEFT OUTER JOIN nationalities + ON players.nationality = nationalities.iso_country_code +NATURAL JOIN player_ranks; + +DROP VIEW ranked_nations; + +CREATE VIEW ranked_nations AS + SELECT + ROW_NUMBER() OVER(ORDER BY score DESC, iso_country_code) AS index, + RANK() OVER(ORDER BY score DESC) AS rank, + score, + iso_country_code, + nation, + continent + FROM nationalities + WHERE score > 0.0; + +CREATE OR REPLACE FUNCTION audit_demon_modification() RETURNS trigger AS $demon_modification_trigger$ +DECLARE + name_change CITEXT; + position_change SMALLINT; + requirement_change SMALLINT; + video_change VARCHAR(200); + thumbnail_change TEXT; + verifier_change INT; + publisher_change INT; +BEGIN + IF (OLD.name <> NEW.name) THEN + name_change = OLD.name; + END IF; + + IF (OLD.position <> NEW.position) THEN + position_change = OLD.position; + END IF; + + IF (OLD.requirement <> NEW.requirement) THEN + requirement_change = OLD.requirement; + END IF; + + IF (OLD.video <> NEW.video) THEN + video_change = OLD.video; + END IF; + + IF (OLD.thumbnail <> NEW.thumbnail) THEN + thumbnail_change = OLD.thumbnail; + END IF; + + IF (OLD.verifier <> NEW.verifier) THEN + verifier_change = OLD.verifier; + END IF; + + IF (OLD.publisher <> NEW.publisher) THEN + publisher_change = OLD.publisher; + END IF; + + INSERT INTO demon_modifications (userid, name, position, requirement, video, verifier, publisher, thumbnail, id) + (SELECT id, name_change, position_change, requirement_change, video_change, verifier_change, publisher_change, thumbnail_change, NEW.id + FROM active_user LIMIT 1); + + RETURN NEW; +END; +$demon_modification_trigger$ LANGUAGE plpgsql; + +SELECT recompute_player_scores(); +SELECT recompute_nation_scores(); +SELECT recompute_subdivision_scores(); + +ALTER TABLE demons DROP COLUMN rated_position; +DROP FUNCTION recompute_rated_positions(); + +ALTER TABLE demons DROP COLUMN rated; +ALTER TABLE players DROP COLUMN unrated_score; +ALTER TABLE nationalities DROP COLUMN unrated_score; +ALTER TABLE subdivisions DROP COLUMN unrated_score; + +ALTER TABLE demon_modifications DROP COLUMN rated; +ALTER TABLE demon_modifications DROP COLUMN rated_position; \ No newline at end of file diff --git a/migrations/20250722002114_unrated_levels.up.sql b/migrations/20250722002114_unrated_levels.up.sql new file mode 100644 index 00000000..b8758643 --- /dev/null +++ b/migrations/20250722002114_unrated_levels.up.sql @@ -0,0 +1,260 @@ +ALTER TABLE demons ADD COLUMN rated BOOLEAN NOT NULL DEFAULT TRUE; +ALTER TABLE players ADD COLUMN unrated_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; +ALTER TABLE nationalities ADD COLUMN unrated_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; +ALTER TABLE subdivisions ADD COLUMN unrated_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; + +ALTER TABLE demons ADD COLUMN rated_position SMALLINT DEFAULT NULL; +UPDATE demons SET rated_position = position; + +ALTER TABLE demons ADD CONSTRAINT unique_rated_position UNIQUE (rated_position) DEFERRABLE INITIALLY DEFERRED; + +CREATE OR REPLACE FUNCTION recompute_rated_positions() RETURNS void AS $$ + BEGIN + UPDATE demons SET rated_position = NULL WHERE NOT rated; + + WITH filtered AS ( + SELECT id, ROW_NUMBER() OVER (ORDER BY demons.position) AS rated_position + FROM demons + WHERE rated + ) + UPDATE demons + SET rated_position = filtered.rated_position + FROM filtered + WHERE demons.id = filtered.id; + END; +$$ LANGUAGE plpgsql; + +SELECT recompute_rated_positions(); + +-- stats viewer stuff +DROP VIEW score_giving; + +CREATE VIEW score_giving AS + SELECT records.progress, demons.position, demons.requirement, records.player, FALSE AS rated_list + FROM records + INNER JOIN demons + ON demons.id = records.demon + WHERE records.status_ = 'APPROVED' AND (demons.position <= 75 OR demons.rated_position <= 75 OR records.progress = 100) + + UNION + + SELECT records.progress, demons.rated_position, demons.requirement, records.player, TRUE AS rated_list + FROM records + INNER JOIN demons + ON demons.id = records.demon + WHERE demons.rated = TRUE AND records.status_ = 'APPROVED' AND (demons.position <= 75 OR demons.rated_position <= 75 OR records.progress = 100) + + UNION + + SELECT 100, demons.position, demons.requirement, demons.verifier, FALSE AS rated_list + FROM demons + + UNION + + SELECT 100, demons.rated_position, demons.requirement, demons.verifier, TRUE AS rated_list + FROM demons + WHERE demons.rated = TRUE; + +DROP FUNCTION score_of_player(INTEGER); +CREATE OR REPLACE FUNCTION score_of_player(is_rated BOOLEAN, player_id INTEGER) RETURNS DOUBLE PRECISION AS $$ + SELECT SUM(record_score(progress, position, 150, requirement)) + FROM score_giving + WHERE player = player_id + AND rated_list = is_rated +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION recompute_player_scores() RETURNS void AS $$ + UPDATE players + SET score = COALESCE(q.score, 0), unrated_score = COALESCE(q.unrated_score, 0) + FROM ( + SELECT player, + SUM(record_score(progress, position, 150, requirement)) + FILTER (WHERE NOT rated_list) AS unrated_score, + SUM(record_score(progress, position, 150, requirement)) + FILTER (WHERE rated_list) AS score + FROM score_giving + GROUP BY player + ) q + WHERE q.player = id; +$$ LANGUAGE SQL; + +DROP FUNCTION score_of_nation(VARCHAR(2)); +CREATE OR REPLACE FUNCTION score_of_nation(is_rated BOOLEAN, iso_country_code VARCHAR(2)) RETURNS DOUBLE PRECISION AS $$ + SELECT SUM(record_score(q.progress, q.position, 150, q.requirement)) + FROM ( + SELECT DISTINCT ON (position) * from score_giving + INNER JOIN players + ON players.id=player + WHERE players.nationality = iso_country_code AND rated_list = is_rated + ORDER BY position, progress DESC + ) q +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION recompute_nation_scores() RETURNS void AS $$ + UPDATE nationalities + SET score = COALESCE(p.score, 0), unrated_score = COALESCE(p.unrated_score, 0) + FROM ( + SELECT nationality, + SUM(record_score(q.progress, q.position, 150, q.requirement)) + FILTER (WHERE q.rated_list) AS score, + SUM(record_score(q.progress, q.position, 150, q.requirement)) + FILTER (WHERE NOT q.rated_list) AS unrated_score + FROM ( + SELECT DISTINCT ON (position, nationality, rated_list) * from score_giving + INNER JOIN players + ON players.id = player + WHERE players.nationality IS NOT NULL + ORDER BY players.nationality, position, rated_list, progress DESC + ) q + GROUP BY nationality + ) p + WHERE p.nationality = iso_country_code +$$ LANGUAGE SQL; + +DROP FUNCTION score_of_subdivision(VARCHAR(2), VARCHAR(3)); +CREATE OR REPLACE FUNCTION score_of_subdivision(is_rated BOOLEAN, iso_country_code VARCHAR(2), iso_code VARCHAR(3)) RETURNS DOUBLE PRECISION AS $$ + SELECT SUM(record_score(q.progress, q.position, 150, q.requirement)) + FROM ( + SELECT DISTINCT ON (position) * from score_giving + INNER JOIN players + ON players.id=player + WHERE players.nationality = iso_country_code + AND players.subdivision = iso_code + AND rated_list = is_rated + ORDER BY position, progress DESC + ) q +$$ LANGUAGE SQL; + +CREATE OR REPLACE FUNCTION recompute_subdivision_scores() RETURNS void AS $$ + UPDATE subdivisions + SET score = COALESCE(p.score, 0), unrated_score = COALESCE(p.unrated_score, 0) + FROM ( + SELECT nationality, subdivision, + SUM(record_score(q.progress, q.position, 150, q.requirement)) + FILTER (WHERE q.rated_list) AS score, + SUM(record_score(q.progress, q.position, 150, q.requirement)) + FILTER (WHERE NOT q.rated_list) AS unrated_score + FROM ( + SELECT DISTINCT ON (position, nationality, subdivision, rated_list) * from score_giving + INNER JOIN players + ON players.id=player + WHERE players.nationality IS NOT NULL + AND players.subdivision IS NOT NULL + ORDER BY players.nationality, players.subdivision, position, rated_list, progress DESC + ) q + GROUP BY nationality, subdivision + ) p + WHERE p.nationality = nation + AND p.subdivision = iso_code +$$ LANGUAGE SQL; + +SELECT recompute_player_scores(); +SELECT recompute_nation_scores(); +SELECT recompute_subdivision_scores(); + +DROP VIEW ranked_players; +DROP MATERIALIZED VIEW player_ranks; + +CREATE MATERIALIZED VIEW player_ranks AS +SELECT + CASE WHEN score != 0 THEN RANK() OVER (ORDER BY score DESC) END AS rank, + CASE WHEN unrated_score != 0 THEN RANK() OVER (ORDER BY unrated_score DESC) END AS unrated_rank, + id +FROM players +WHERE unrated_score != 0 OR score != 0 AND NOT banned; + +CREATE UNIQUE INDEX player_ranks_id_idx ON player_ranks(id); + +CREATE VIEW ranked_players AS +SELECT + ROW_NUMBER() OVER(ORDER BY rank, id) AS index, + ROW_NUMBER() OVER (ORDER BY unrated_rank, id) AS unrated_index, + rank, + unrated_rank, + id, name, players.score, players.unrated_score, + subdivision, + nationalities.iso_country_code, + nationalities.nation, + nationalities.continent +FROM players +LEFT OUTER JOIN nationalities + ON players.nationality = nationalities.iso_country_code +NATURAL JOIN player_ranks; + +DROP VIEW ranked_nations; + +CREATE VIEW ranked_nations AS + SELECT + ROW_NUMBER() OVER (ORDER BY score DESC, iso_country_code) AS index, + ROW_NUMBER() OVER (ORDER BY unrated_score DESC, iso_country_code) AS unrated_index, + CASE WHEN score != 0 THEN RANK() OVER (ORDER BY score DESC) END AS rank, + CASE WHEN unrated_score != 0 THEN RANK() OVER (ORDER BY unrated_score DESC) END AS unrated_rank, + score, + unrated_score, + iso_country_code, + nation, + continent + FROM nationalities + WHERE score > 0.0 OR unrated_score > 0.0; + +-- audit log stuff +ALTER TABLE demon_modifications ADD COLUMN rated BOOLEAN NULL DEFAULT NULL; +ALTER TABLE demon_modifications ADD COLUMN rated_position SMALLINT NULL DEFAULT NULL; + +UPDATE demon_modifications SET rated_position = position; + +CREATE OR REPLACE FUNCTION audit_demon_modification() RETURNS trigger AS $demon_modification_trigger$ +DECLARE + name_change CITEXT; + position_change SMALLINT; + rated_position_change SMALLINT; + requirement_change SMALLINT; + video_change VARCHAR(200); + thumbnail_change TEXT; + verifier_change INT; + publisher_change INT; + rated_change BOOLEAN; +BEGIN + IF (OLD.name <> NEW.name) THEN + name_change = OLD.name; + END IF; + + IF (OLD.position <> NEW.position) THEN + position_change = OLD.position; + END IF; + + IF (OLD.rated_position IS DISTINCT FROM NEW.rated_position) THEN + rated_position_change = OLD.rated_position; + END IF; + + IF (OLD.requirement <> NEW.requirement) THEN + requirement_change = OLD.requirement; + END IF; + + IF (OLD.video <> NEW.video) THEN + video_change = OLD.video; + END IF; + + IF (OLD.thumbnail <> NEW.thumbnail) THEN + thumbnail_change = OLD.thumbnail; + END IF; + + IF (OLD.verifier <> NEW.verifier) THEN + verifier_change = OLD.verifier; + END IF; + + IF (OLD.publisher <> NEW.publisher) THEN + publisher_change = OLD.publisher; + END IF; + + IF (OLD.rated <> NEW.rated) THEN + rated_change = OLD.rated; + END IF; + + INSERT INTO demon_modifications (userid, name, position, rated_position, requirement, video, verifier, publisher, thumbnail, rated, id) + (SELECT id, name_change, position_change, rated_position_change, requirement_change, video_change, verifier_change, publisher_change, thumbnail_change, rated_change, NEW.id + FROM active_user LIMIT 1); + + RETURN NEW; +END; +$demon_modification_trigger$ LANGUAGE plpgsql; From 2be11eb5095b482009990371db38165253a3899c Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 15:59:10 -0400 Subject: [PATCH 02/19] (migrations) updated time machine to consider level rating statuses --- ...250824144746_unrated_time_machine.down.sql | 29 ++++++++++++++ ...20250824144746_unrated_time_machine.up.sql | 39 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 migrations/20250824144746_unrated_time_machine.down.sql create mode 100644 migrations/20250824144746_unrated_time_machine.up.sql diff --git a/migrations/20250824144746_unrated_time_machine.down.sql b/migrations/20250824144746_unrated_time_machine.down.sql new file mode 100644 index 00000000..2998569c --- /dev/null +++ b/migrations/20250824144746_unrated_time_machine.down.sql @@ -0,0 +1,29 @@ +DROP FUNCTION list_at(BOOLEAN, TIMESTAMP WITHOUT TIME ZONE); + +CREATE FUNCTION list_at(TIMESTAMP WITHOUT TIME ZONE) + RETURNS TABLE ( + name CITEXT, + position_ SMALLINT, + requirement SMALLINT, + video VARCHAR(200), + thumbnail TEXT, + verifier INTEGER, + publisher INTEGER, + id INTEGER, + level_id BIGINT, + current_position SMALLINT + ) +AS $$ +SELECT name, CASE WHEN t.position IS NULL THEN demons.position ELSE t.position END, requirement, video, thumbnail, verifier, publisher, demons.id, level_id, demons.position AS current_position +FROM demons + LEFT OUTER JOIN ( + SELECT DISTINCT ON (id) id, position + FROM demon_modifications + WHERE time >= $1 AND position != -1 + ORDER BY id, time +) t + ON demons.id = t.id +WHERE NOT EXISTS (SELECT 1 FROM demon_additions WHERE demon_additions.id = demons.id AND time >= $1) +$$ + LANGUAGE SQL + STABLE; \ No newline at end of file diff --git a/migrations/20250824144746_unrated_time_machine.up.sql b/migrations/20250824144746_unrated_time_machine.up.sql new file mode 100644 index 00000000..908bf9ab --- /dev/null +++ b/migrations/20250824144746_unrated_time_machine.up.sql @@ -0,0 +1,39 @@ +DROP FUNCTION list_at(TIMESTAMP WITHOUT TIME ZONE); + +CREATE FUNCTION list_at(is_rated_list BOOLEAN, TIMESTAMP WITHOUT TIME ZONE) + RETURNS TABLE ( + name CITEXT, + position_ SMALLINT, + requirement SMALLINT, + video VARCHAR(200), + thumbnail TEXT, + verifier INTEGER, + publisher INTEGER, + id INTEGER, + level_id BIGINT, + rated BOOLEAN, + current_position SMALLINT + ) +AS $$ +SELECT name, COALESCE(t.position, CASE WHEN is_rated_list THEN demons.rated_position ELSE demons.position END) as position_, requirement, video, thumbnail, verifier, publisher, demons.id, level_id, COALESCE(r.rated, demons.rated), +CASE WHEN is_rated_list THEN demons.rated_position ELSE demons.position END AS current_position +FROM demons +LEFT OUTER JOIN ( + SELECT DISTINCT ON (id) id, CASE WHEN is_rated_list THEN rated_position ELSE position END AS position + FROM demon_modifications + WHERE time >= $2 AND (is_rated_list AND rated_position IS NOT NULL) OR (NOT is_rated_list AND position != -1) + ORDER BY id, time +) t +ON demons.id = t.id +LEFT OUTER JOIN ( + SELECT DISTINCT ON (id) id, rated + FROM demon_modifications + WHERE time >= $2 AND rated IS NOT NULL + ORDER BY id, time +) r +ON demons.id = r.id +WHERE NOT EXISTS (SELECT 1 FROM demon_additions WHERE demon_additions.id = demons.id AND time >= $2) +AND (NOT is_rated_list OR COALESCE(r.rated, demons.rated)) +$$ + LANGUAGE SQL + STABLE; From 28123b41d6835ae96b8be94befac54148417ff5b Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:14:56 -0400 Subject: [PATCH 03/19] updated demon structures + modifications to related queries --- pointercrate-demonlist/sql/all_demons.sql | 2 +- pointercrate-demonlist/sql/all_demons_at.sql | 4 +- .../sql/all_rated_demons.sql | 8 ++ pointercrate-demonlist/sql/demon_by_id.sql | 2 +- pointercrate-demonlist/sql/demon_by_name.sql | 2 +- .../sql/demon_by_position.sql | 4 +- .../sql/paginate_demons_by_id.sql | 2 +- .../sql/paginate_demons_by_position.sql | 2 +- .../sql/paginate_player_ranking.sql | 9 ++- .../sql/paginate_players_by_id.sql | 2 +- .../sql/paginate_rated_player_ranking.sql | 11 +++ .../sql/paginate_records.sql | 5 +- pointercrate-demonlist/sql/record_by_id.sql | 2 +- pointercrate-demonlist/src/creator/get.rs | 2 +- pointercrate-demonlist/src/demon/get.rs | 74 +++++++++++++------ pointercrate-demonlist/src/demon/mod.rs | 40 +++++++++- pointercrate-demonlist/src/demon/paginate.rs | 4 + pointercrate-demonlist/src/demon/patch.rs | 23 +++++- pointercrate-demonlist/src/demon/post.rs | 17 ++++- pointercrate-demonlist/src/nationality/get.rs | 17 +++-- pointercrate-demonlist/src/nationality/mod.rs | 4 +- pointercrate-demonlist/src/record/get.rs | 5 +- pointercrate-demonlist/src/record/paginate.rs | 14 ++++ 23 files changed, 198 insertions(+), 57 deletions(-) create mode 100644 pointercrate-demonlist/sql/all_rated_demons.sql create mode 100644 pointercrate-demonlist/sql/paginate_rated_player_ranking.sql diff --git a/pointercrate-demonlist/sql/all_demons.sql b/pointercrate-demonlist/sql/all_demons.sql index d22287f8..173d2e4e 100644 --- a/pointercrate-demonlist/sql/all_demons.sql +++ b/pointercrate-demonlist/sql/all_demons.sql @@ -1,4 +1,4 @@ -SELECT demons.id AS "demon_id!", demons.name AS "demon_name!: String", demons.position as "position!", demons.requirement as "requirement!", demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, verifiers.id AS "verifier_id!", verifiers.name AS "verifier_name!: String", verifiers.banned AS "verifier_banned!", publishers.id AS "publisher_id!", publishers.name AS "publisher_name!: String", publishers.banned AS "publisher_banned!" +SELECT demons.id AS "demon_id!", demons.name AS "demon_name!: String", demons.position as "position!", demons.rated_position, demons.requirement as "requirement!", demons.level_id, demons.rated, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, verifiers.id AS "verifier_id!", verifiers.name AS "verifier_name!: String", verifiers.banned AS "verifier_banned!", publishers.id AS "publisher_id!", publishers.name AS "publisher_name!: String", publishers.banned AS "publisher_banned!" FROM demons INNER JOIN players as publishers ON demons.publisher = publishers.id diff --git a/pointercrate-demonlist/sql/all_demons_at.sql b/pointercrate-demonlist/sql/all_demons_at.sql index 4d6d0ec7..cb145546 100644 --- a/pointercrate-demonlist/sql/all_demons_at.sql +++ b/pointercrate-demonlist/sql/all_demons_at.sql @@ -1,5 +1,5 @@ -SELECT demons.id AS "demon_id!", demons.name AS "demon_name!: String", demons.position_ as "position!", demons.requirement as "requirement!", demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail AS "thumbnail!", verifiers.id AS "verifier_id!", verifiers.name AS "verifier_name!: String", verifiers.banned AS "verifier_banned!", publishers.id AS "publisher_id!", publishers.name AS "publisher_name!: String", publishers.banned AS "publisher_banned!", demons.current_position as "current_position!" -FROM list_at($1) AS demons +SELECT demons.id AS "demon_id!", demons.name AS "demon_name!: String", demons.position_ as "position!", demons.requirement as "requirement!", demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail AS "thumbnail!", verifiers.id AS "verifier_id!", verifiers.name AS "verifier_name!: String", verifiers.banned AS "verifier_banned!", publishers.id AS "publisher_id!", publishers.name AS "publisher_name!: String", publishers.banned AS "publisher_banned!", demons.rated as "rated!", demons.current_position +FROM list_at($2, $1) AS demons INNER JOIN players as publishers ON demons.publisher = publishers.id INNER JOIN players AS verifiers diff --git a/pointercrate-demonlist/sql/all_rated_demons.sql b/pointercrate-demonlist/sql/all_rated_demons.sql new file mode 100644 index 00000000..91e0df5c --- /dev/null +++ b/pointercrate-demonlist/sql/all_rated_demons.sql @@ -0,0 +1,8 @@ +SELECT demons.id AS "demon_id!", demons.name AS "demon_name!: String", demons.position as "position!", demons.rated_position, demons.requirement as "requirement!", demons.level_id, demons.rated, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, verifiers.id AS "verifier_id!", verifiers.name AS "verifier_name!: String", verifiers.banned AS "verifier_banned!", publishers.id AS "publisher_id!", publishers.name AS "publisher_name!: String", publishers.banned AS "publisher_banned!" +FROM demons + INNER JOIN players as publishers + ON demons.publisher = publishers.id + INNER JOIN players AS verifiers + ON demons.verifier = verifiers.id +WHERE rated +ORDER BY position \ No newline at end of file diff --git a/pointercrate-demonlist/sql/demon_by_id.sql b/pointercrate-demonlist/sql/demon_by_id.sql index 7510111d..1de6c789 100644 --- a/pointercrate-demonlist/sql/demon_by_id.sql +++ b/pointercrate-demonlist/sql/demon_by_id.sql @@ -1,4 +1,4 @@ -SELECT demons.id AS demon_id, demons.name AS "demon_name: String", demons.position, demons.requirement, demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, +SELECT demons.id AS demon_id, demons.name AS "demon_name: String", demons.position, demons.rated_position, demons.requirement, demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, demons.rated, verifiers.id AS verifier_id, verifiers.name AS "verifier_name: String", verifiers.banned AS verifier_banned, publishers.id AS publisher_id, publishers.name AS "publisher_name: String", publishers.banned AS publisher_banned FROM demons diff --git a/pointercrate-demonlist/sql/demon_by_name.sql b/pointercrate-demonlist/sql/demon_by_name.sql index 90a6254d..40862527 100644 --- a/pointercrate-demonlist/sql/demon_by_name.sql +++ b/pointercrate-demonlist/sql/demon_by_name.sql @@ -1,4 +1,4 @@ -SELECT demons.id AS demon_id, demons.name AS "demon_name: String", demons.position, demons.requirement, demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, +SELECT demons.id AS demon_id, demons.name AS "demon_name: String", demons.position, demons.rated_position, demons.requirement, demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, demons.rated, verifiers.id AS verifier_id, verifiers.name AS "verifier_name: String", verifiers.banned AS verifier_banned, publishers.id AS publisher_id, publishers.name AS "publisher_name: String", publishers.banned AS publisher_banned FROM demons diff --git a/pointercrate-demonlist/sql/demon_by_position.sql b/pointercrate-demonlist/sql/demon_by_position.sql index 71c7261e..805dbc74 100644 --- a/pointercrate-demonlist/sql/demon_by_position.sql +++ b/pointercrate-demonlist/sql/demon_by_position.sql @@ -1,7 +1,7 @@ -SELECT demons.id AS demon_id, demons.name AS "demon_name: String", demons.position, demons.requirement, demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video END, demons.thumbnail, +SELECT demons.id AS demon_id, demons.name AS "demon_name: String", demons.position, demons.rated_position, demons.requirement, demons.level_id, demons.rated, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video END, demons.thumbnail, verifiers.id AS verifier_id, verifiers.name AS "verifier_name: String", verifiers.banned AS verifier_banned, publishers.id AS publisher_id, publishers.name AS "publisher_name: String", publishers.banned AS publisher_banned FROM demons INNER JOIN players AS verifiers ON verifiers.id=demons.verifier INNER JOIN players AS publishers ON publishers.id=demons.publisher -WHERE demons.position=$1 \ No newline at end of file +WHERE (demons.position=$1 AND $2) OR (demons.rated_position=$1 AND NOT $2) \ No newline at end of file diff --git a/pointercrate-demonlist/sql/paginate_demons_by_id.sql b/pointercrate-demonlist/sql/paginate_demons_by_id.sql index 95535d12..2ed62835 100644 --- a/pointercrate-demonlist/sql/paginate_demons_by_id.sql +++ b/pointercrate-demonlist/sql/paginate_demons_by_id.sql @@ -1,4 +1,4 @@ -SELECT demons.id AS demon_id, demons.name::text AS demon_name, demons.position, demons.requirement, demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, +SELECT demons.id AS demon_id, demons.name::text AS demon_name, demons.position, demons.rated_position, demons.requirement, demons.level_id, demons.rated, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END, demons.thumbnail, verifiers.id AS verifier_id, verifiers.name::text AS verifier_name, verifiers.banned AS verifier_banned, publishers.id AS publisher_id, publishers.name::text AS publisher_name, publishers.banned AS publisher_banned FROM demons diff --git a/pointercrate-demonlist/sql/paginate_demons_by_position.sql b/pointercrate-demonlist/sql/paginate_demons_by_position.sql index 45df9fe9..425a1d45 100644 --- a/pointercrate-demonlist/sql/paginate_demons_by_position.sql +++ b/pointercrate-demonlist/sql/paginate_demons_by_position.sql @@ -1,4 +1,4 @@ -SELECT demons.id AS demon_id, demons.name::text AS demon_name, demons.position, demons.requirement, demons.level_id, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END,demons.thumbnail, +SELECT demons.id AS demon_id, demons.name::text AS demon_name, demons.position, demons.rated_position, demons.requirement, demons.level_id, demons.rated, CASE WHEN verifiers.link_banned THEN NULL ElSE demons.video::text END,demons.thumbnail, verifiers.id AS verifier_id, verifiers.name::text AS verifier_name, verifiers.banned AS verifier_banned, publishers.id AS publisher_id, publishers.name::text AS publisher_name, publishers.banned AS publisher_banned FROM demons diff --git a/pointercrate-demonlist/sql/paginate_player_ranking.sql b/pointercrate-demonlist/sql/paginate_player_ranking.sql index 9fd2b94f..98e4841f 100644 --- a/pointercrate-demonlist/sql/paginate_player_ranking.sql +++ b/pointercrate-demonlist/sql/paginate_player_ranking.sql @@ -1,10 +1,11 @@ -SELECT index, rank, id, name, score, subdivision, iso_country_code, nation +SELECT unrated_index as index, unrated_rank as rank, id, name, unrated_score as score, subdivision, iso_country_code, nation FROM ranked_players -WHERE (index < $1 OR $1 IS NULL) - AND (index > $2 OR $2 IS NULL) +WHERE unrated_rank IS NOT NULL + AND (unrated_index < $1 OR $1 IS NULL) + AND (unrated_index > $2 OR $2 IS NULL) AND (STRPOS(name, $3::CITEXT) > 0 OR $3 is NULL) AND (nation = $4 OR iso_country_code = $4 OR (nation IS NULL AND $5) OR ($4 IS NULL AND NOT $5)) AND (continent = CAST($6::TEXT AS continent) OR $6 IS NULL) AND (subdivision = $7 OR $7 IS NULL) -ORDER BY rank {}, id +ORDER BY unrated_rank {}, id LIMIT $8 \ No newline at end of file diff --git a/pointercrate-demonlist/sql/paginate_players_by_id.sql b/pointercrate-demonlist/sql/paginate_players_by_id.sql index a1ddd12c..402b859b 100644 --- a/pointercrate-demonlist/sql/paginate_players_by_id.sql +++ b/pointercrate-demonlist/sql/paginate_players_by_id.sql @@ -1,4 +1,4 @@ -SELECT players.id, players.name::TEXT, banned, nationalities.nation::TEXT, iso_country_code::TEXT, subdivision::TEXT AS iso_code, subdivisions.name AS subdivision_name, players.score, player_ranks.rank +SELECT players.id, players.name::TEXT, banned, nationalities.nation::TEXT, iso_country_code::TEXT, subdivision::TEXT AS iso_code, subdivisions.name AS subdivision_name, players.score, players.unrated_score, player_ranks.rank, player_ranks.unrated_rank FROM players LEFT OUTER JOIN nationalities ON nationality = iso_country_code LEFT OUTER JOIN subdivisions ON iso_code = subdivision AND subdivisions.nation = nationality diff --git a/pointercrate-demonlist/sql/paginate_rated_player_ranking.sql b/pointercrate-demonlist/sql/paginate_rated_player_ranking.sql new file mode 100644 index 00000000..fc10876a --- /dev/null +++ b/pointercrate-demonlist/sql/paginate_rated_player_ranking.sql @@ -0,0 +1,11 @@ +SELECT index, rank, id, name, score, subdivision, iso_country_code, nation +FROM ranked_players +WHERE rank IS NOT NULL + AND (index < $1 OR $1 IS NULL) + AND (index > $2 OR $2 IS NULL) + AND (STRPOS(name, $3::CITEXT) > 0 OR $3 is NULL) + AND (nation = $4 OR iso_country_code = $4 OR (nation IS NULL AND $5) OR ($4 IS NULL AND NOT $5)) + AND (continent = CAST($6::TEXT AS continent) OR $6 IS NULL) + AND (subdivision = $7 OR $7 IS NULL) +ORDER BY rank {}, id +LIMIT $8 \ No newline at end of file diff --git a/pointercrate-demonlist/sql/paginate_records.sql b/pointercrate-demonlist/sql/paginate_records.sql index ea2170bb..44051610 100644 --- a/pointercrate-demonlist/sql/paginate_records.sql +++ b/pointercrate-demonlist/sql/paginate_records.sql @@ -1,6 +1,6 @@ SELECT records.id, progress, CASE WHEN players.link_banned THEN NULL ELSE records.video::text END, status_::text AS status, players.id AS player_id, players.name::text AS player_name, players.banned AS player_banned, - demons.id AS demon_id, demons.name::text AS demon_name, demons.position + demons.id AS demon_id, demons.name::text AS demon_name, demons.position, demons.rated_position FROM records INNER JOIN players ON records.player = players.id INNER JOIN demons ON records.demon = demons.id @@ -18,5 +18,8 @@ WHERE (records.id < $1 OR $1 IS NULL) AND (records.video = $12 OR (records.video IS NULL AND $13) OR ($12 IS NULL AND NOT $13)) AND (players.id = $14 OR $14 IS NULL) AND (records.submitter = $15 OR $15 IS NULL) + AND (rated_position = $17 OR $17 IS NULL) + AND (rated_position < $18 OR $18 IS NULL) + AND (rated_position > $19 OR $19 IS NULL) ORDER BY id {} LIMIT $16 diff --git a/pointercrate-demonlist/sql/record_by_id.sql b/pointercrate-demonlist/sql/record_by_id.sql index dc13ddd8..87b47fb8 100644 --- a/pointercrate-demonlist/sql/record_by_id.sql +++ b/pointercrate-demonlist/sql/record_by_id.sql @@ -3,7 +3,7 @@ SELECT progress, CASE WHEN players.link_banned THEN NULL ELSE records.raw_footage::text END, status_::text AS "status!: String" , players.id AS player_id, players.name AS "player_name: String", players.banned AS player_banned, - demons.id AS demon_id, demons.name AS "demon_name: String", demons.position, + demons.id AS demon_id, demons.name AS "demon_name: String", demons.position, demons.rated_position, submitters.submitter_id AS submitter_id, submitters.banned AS submitter_banned FROM records INNER JOIN players ON records.player = players.id diff --git a/pointercrate-demonlist/src/creator/get.rs b/pointercrate-demonlist/src/creator/get.rs index d9796ef2..8b98e43e 100644 --- a/pointercrate-demonlist/src/creator/get.rs +++ b/pointercrate-demonlist/src/creator/get.rs @@ -57,7 +57,7 @@ pub async fn creators_of(demon: &MinimalDemon, connection: &mut PgConnection) -> pub async fn created_by(player_id: i32, connection: &mut PgConnection) -> Result> { query_many_demons!( connection, - r#"SELECT demons.id, demons.name, demons.position FROM demons INNER JOIN creators ON demons.id = creators.demon WHERE + r#"SELECT demons.id, demons.name, demons.position, demons.rated_position FROM demons INNER JOIN creators ON demons.id = creators.demon WHERE creators.creator=$1"#, player_id ) diff --git a/pointercrate-demonlist/src/demon/get.rs b/pointercrate-demonlist/src/demon/get.rs index 09858e04..56f221e8 100644 --- a/pointercrate-demonlist/src/demon/get.rs +++ b/pointercrate-demonlist/src/demon/get.rs @@ -2,6 +2,7 @@ use crate::{ creator::creators_of, demon::{Demon, FullDemon, MinimalDemon, TimeShiftedDemon}, error::{DemonlistError, Result}, + list::List, player::DatabasePlayer, record::approved_records_on, }; @@ -11,17 +12,25 @@ use sqlx::{Error, PgConnection}; impl MinimalDemon { pub async fn by_id(id: i32, connection: &mut PgConnection) -> Result { - sqlx::query_as!(MinimalDemon, r#"SELECT id, name, position FROM demons WHERE id = $1"#, id) - .fetch_one(connection) - .await - .map_err(|err| match err { - Error::RowNotFound => DemonlistError::DemonNotFound { demon_id: id }, - _ => err.into(), - }) + sqlx::query_as!( + MinimalDemon, + r#"SELECT id, name, position, rated_position FROM demons WHERE id = $1"#, + id + ) + .fetch_one(connection) + .await + .map_err(|err| match err { + Error::RowNotFound => DemonlistError::DemonNotFound { demon_id: id }, + _ => err.into(), + }) } pub async fn by_name(name: &str, connection: &mut PgConnection) -> Result { - let mut stream = sqlx::query!(r#"SELECT id, name, position FROM demons WHERE name = $1"#, name.to_string()).fetch(connection); + let mut stream = sqlx::query!( + r#"SELECT id, name, position, rated_position FROM demons WHERE name = $1"#, + name.to_string() + ) + .fetch(connection); let mut demon = None; let mut further_demons = Vec::new(); @@ -32,6 +41,7 @@ impl MinimalDemon { let current_demon = MinimalDemon { id: row.id, position: row.position, + rated_position: row.rated_position, name: row.name, }; @@ -62,8 +72,8 @@ impl FullDemon { Demon::by_id(id, connection).await?.upgrade(connection).await } - pub async fn by_position(position: i16, connection: &mut PgConnection) -> Result { - Demon::by_position(position, connection).await?.upgrade(connection).await + pub async fn by_position(position: i16, list: &List, connection: &mut PgConnection) -> Result { + Demon::by_position(position, list, connection).await?.upgrade(connection).await } } @@ -91,8 +101,8 @@ impl Demon { }) } - pub async fn by_position(position: i16, connection: &mut PgConnection) -> Result { - sqlx::query_file_as!(FetchedDemon, "sql/demon_by_position.sql", position) + pub async fn by_position(position: i16, list: &List, connection: &mut PgConnection) -> Result { + sqlx::query_file_as!(FetchedDemon, "sql/demon_by_position.sql", position, *list == List::RatedPlus) .fetch_one(connection) .await .map(Into::into) @@ -119,7 +129,7 @@ macro_rules! query_many_demons { pub async fn published_by(player: &DatabasePlayer, connection: &mut PgConnection) -> Result> { query_many_demons!( connection, - r#"SELECT id, name, position FROM demons WHERE publisher = $1"#, + r#"SELECT id, name, position, rated_position FROM demons WHERE publisher = $1"#, player.id ) } @@ -127,7 +137,7 @@ pub async fn published_by(player: &DatabasePlayer, connection: &mut PgConnection pub async fn verified_by(player: &DatabasePlayer, connection: &mut PgConnection) -> Result> { query_many_demons!( connection, - r#"SELECT id, name, position FROM demons WHERE verifier = $1"#, + r#"SELECT id, name, position, rated_position FROM demons WHERE verifier = $1"#, player.id ) } @@ -136,6 +146,7 @@ struct FetchedDemon { demon_id: i32, demon_name: String, position: i16, + rated_position: Option, requirement: i16, video: Option, thumbnail: String, @@ -146,6 +157,7 @@ struct FetchedDemon { verifier_name: String, verifier_banned: bool, level_id: Option, + rated: bool, } impl From for Demon { @@ -155,6 +167,7 @@ impl From for Demon { id: fetched.demon_id, name: fetched.demon_name, position: fetched.position, + rated_position: fetched.rated_position, }, requirement: fetched.requirement, video: fetched.video, @@ -170,21 +183,31 @@ impl From for Demon { banned: fetched.verifier_banned, }, level_id: fetched.level_id.map(|id| id as u64), + rated: fetched.rated, } } } -pub async fn current_list(connection: &mut PgConnection) -> Result> { - Ok(sqlx::query_file_as!(FetchedDemon, "sql/all_demons.sql") - .fetch_all(connection) - .await? - .into_iter() - .map(Into::into) - .collect()) +pub async fn current_list(list: &List, connection: &mut PgConnection) -> Result> { + Ok(match list { + List::Demonlist => { + sqlx::query_file_as!(FetchedDemon, "sql/all_rated_demons.sql") + .fetch_all(connection) + .await? + }, + List::RatedPlus => { + sqlx::query_file_as!(FetchedDemon, "sql/all_demons.sql") + .fetch_all(connection) + .await? + }, + } + .into_iter() + .map(Into::into) + .collect()) } -pub async fn list_at(connection: &mut PgConnection, at: NaiveDateTime) -> Result> { - let mut stream = sqlx::query_file!("sql/all_demons_at.sql", at).fetch(connection); +pub async fn list_at(connection: &mut PgConnection, list: &List, at: NaiveDateTime) -> Result> { + let mut stream = sqlx::query_file!("sql/all_demons_at.sql", at, *list == List::Demonlist).fetch(connection); let mut demons = Vec::new(); while let Some(row) = stream.next().await { @@ -194,7 +217,11 @@ pub async fn list_at(connection: &mut PgConnection, at: NaiveDateTime) -> Result current_demon: Demon { base: MinimalDemon { id: row.demon_id, + // fixme: the query already retrieves the demon positions + // for the list this struct is used in, so we can just set + // these to the same thing position: row.position, + rated_position: Some(row.position), name: row.demon_name, }, requirement: row.requirement, @@ -211,6 +238,7 @@ pub async fn list_at(connection: &mut PgConnection, at: NaiveDateTime) -> Result banned: row.verifier_banned, }, level_id: row.level_id.map(|i| i as u64), + rated: row.rated, }, position_now: row.current_position, }) diff --git a/pointercrate-demonlist/src/demon/mod.rs b/pointercrate-demonlist/src/demon/mod.rs index 9697637c..4d6fa43b 100644 --- a/pointercrate-demonlist/src/demon/mod.rs +++ b/pointercrate-demonlist/src/demon/mod.rs @@ -6,6 +6,7 @@ pub use self::{ }; use crate::{ error::{DemonlistError, Result}, + list::List, player::DatabasePlayer, record::MinimalRecordP, }; @@ -26,9 +27,15 @@ mod paginate; mod patch; mod post; +pub async fn recompute_rated_positions(connection: &mut PgConnection) -> Result<()> { + sqlx::query("SELECT recompute_rated_positions()").execute(connection).await?; + + Ok(()) +} + pub struct TimeShiftedDemon { pub current_demon: Demon, - pub position_now: i16, + pub position_now: Option, } /// Struct modelling a demon. These objects are returned from the paginating `/demons/` endpoint @@ -57,6 +64,9 @@ pub struct Demon { /// This is automatically queried based on the level name, but can be manually overridden by a /// list mod. pub level_id: Option, + + /// Whether this [`Demon`] has a star rating in Geometry Dash + pub rated: bool, } /// Absolutely minimal representation of a demon to be sent when a demon is part of another object @@ -71,6 +81,9 @@ pub struct MinimalDemon { /// Positions for consecutive demons are always consecutive positive integers pub position: i16, + /// The [`Demon`]'s position on the Rated+ list + pub rated_position: Option, + /// The [`Demon`]'s Geometry Dash level name /// /// Note that the name doesn't need to be unique! @@ -107,6 +120,24 @@ impl MinimalDemon { .await? .requirement) } + + /// Utility function for getting the position of a demon on a particular list + pub fn position(&self, list: &List) -> Option { + match *list { + List::Demonlist => self.rated_position, + List::RatedPlus => Some(self.position), + } + } + + /// Returns whether this demon is on the main list for all the lists it's on + pub fn is_any_main(&self) -> bool { + self.position <= crate::config::list_size() || self.rated_position.is_some_and(|p| p <= crate::config::list_size()) + } + + /// Returns whether this demon is on the legacy list for all the lists it's on + pub fn is_all_legacy(&self) -> bool { + self.position > crate::config::extended_list_size() && self.rated_position.map_or(true, |p| p > crate::config::extended_list_size()) + } } impl FullDemon { @@ -170,12 +201,15 @@ impl Demon { .unwrap_or(0)) } - pub fn score(&self, progress: i16) -> f64 { + pub fn score(&self, list: &List, progress: i16) -> f64 { if progress < self.requirement { return 0.0; } - let position = self.base.position; + let position = match *list { + List::RatedPlus => self.base.position, + List::Demonlist => self.base.rated_position.unwrap_or_else(|| return 0), + }; let beaten_score = match position { 56..=150 => 1.039035131_f64 * ((185.7_f64 * (-0.02715_f64 * position as f64).exp()) + 14.84_f64), diff --git a/pointercrate-demonlist/src/demon/paginate.rs b/pointercrate-demonlist/src/demon/paginate.rs index 9a070cb5..137f31c5 100644 --- a/pointercrate-demonlist/src/demon/paginate.rs +++ b/pointercrate-demonlist/src/demon/paginate.rs @@ -97,6 +97,7 @@ impl Paginatable for Demon { id: row.get("demon_id"), name: row.get("demon_name"), position: row.get("position"), + rated_position: row.get("rated_position"), }, requirement: row.get("requirement"), video, @@ -112,6 +113,7 @@ impl Paginatable for Demon { banned: row.get("verifier_banned"), }, level_id: row.get::, _>("level_id").map(|id| id as u64), + rated: row.get("rated"), }) } @@ -209,6 +211,7 @@ impl Paginatable for Demon { id: row.get("demon_id"), name: row.get("demon_name"), position: row.get("position"), + rated_position: row.get("rated_position"), }, requirement: row.get("requirement"), video, @@ -224,6 +227,7 @@ impl Paginatable for Demon { banned: row.get("verifier_banned"), }, level_id: row.get::, _>("level_id").map(|id| id as u64), + rated: row.get("rated"), }) } diff --git a/pointercrate-demonlist/src/demon/patch.rs b/pointercrate-demonlist/src/demon/patch.rs index c6cc16fc..b2115b7a 100644 --- a/pointercrate-demonlist/src/demon/patch.rs +++ b/pointercrate-demonlist/src/demon/patch.rs @@ -1,5 +1,5 @@ use crate::{ - demon::{Demon, FullDemon, MinimalDemon}, + demon::{recompute_rated_positions, Demon, FullDemon, MinimalDemon}, error::{DemonlistError, Result}, player::{recompute_scores, DatabasePlayer}, }; @@ -30,6 +30,9 @@ pub struct PatchDemon { #[serde(default, deserialize_with = "non_nullable")] pub publisher: Option, + + #[serde(default, deserialize_with = "non_nullable")] + pub rated: Option, } impl FullDemon { @@ -89,6 +92,10 @@ impl Demon { self.set_requirement(requirement, connection).await?; } + if let Some(rated) = patch.rated { + self.set_rated(rated, connection).await?; + } + Ok(self) } @@ -169,6 +176,19 @@ impl Demon { Ok(()) } + + pub async fn set_rated(&mut self, rated: bool, connection: &mut PgConnection) -> Result<()> { + sqlx::query!("UPDATE demons SET rated = $1 WHERE id = $2", rated, self.base.id) + .execute(&mut *connection) + .await?; + + self.rated = rated; + + recompute_rated_positions(connection).await?; + recompute_scores(connection).await?; + + Ok(()) + } } impl MinimalDemon { @@ -247,6 +267,7 @@ impl MinimalDemon { self.position = to; + recompute_rated_positions(&mut *connection).await?; recompute_scores(connection).await?; Ok(()) diff --git a/pointercrate-demonlist/src/demon/post.rs b/pointercrate-demonlist/src/demon/post.rs index f7bccbbe..357439b1 100644 --- a/pointercrate-demonlist/src/demon/post.rs +++ b/pointercrate-demonlist/src/demon/post.rs @@ -1,6 +1,6 @@ use crate::{ creator::Creator, - demon::{Demon, FullDemon, MinimalDemon}, + demon::{recompute_rated_positions, Demon, FullDemon, MinimalDemon}, error::Result, player::{recompute_scores, DatabasePlayer}, }; @@ -18,6 +18,7 @@ pub struct PostDemon { creators: Vec, video: Option, level_id: Option, + rated: bool, } impl FullDemon { @@ -41,15 +42,16 @@ impl FullDemon { Demon::shift_down(data.position, connection).await?; let created = sqlx::query!( - "INSERT INTO demons (name, position, requirement, video, verifier, publisher, level_id) VALUES ($1::text,$2,$3,$4::text,$5,$6, $7) \ - RETURNING id, thumbnail", + "INSERT INTO demons (name, position, requirement, video, verifier, publisher, level_id, rated) VALUES ($1::text,$2,$3,$4::text,$5,$6, $7, $8) \ + RETURNING id, thumbnail, rated_position", data.name.to_string(), data.position, data.requirement, video.as_ref(), verifier.id, publisher.id, - data.level_id + data.level_id, + data.rated, ) .fetch_one(&mut *connection) .await?; @@ -58,6 +60,7 @@ impl FullDemon { base: MinimalDemon { id: created.id, position: data.position, + rated_position: created.rated_position, name: data.name, }, requirement: data.requirement, @@ -66,6 +69,7 @@ impl FullDemon { publisher, verifier, level_id, + rated: data.rated, }; let mut creators = Vec::new(); @@ -77,6 +81,7 @@ impl FullDemon { creators.push(player); } + recompute_rated_positions(connection).await?; recompute_scores(connection).await?; Ok(FullDemon { @@ -110,6 +115,7 @@ mod tests { creators: Vec::new(), video: None, level_id: None, + rated: true, }, &mut conn, ) @@ -136,6 +142,7 @@ mod tests { creators: Vec::new(), video: Some("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_owned()), level_id: None, + rated: true, }, &mut conn, ) @@ -157,6 +164,7 @@ mod tests { creators: Vec::new(), video: Some("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_owned()), level_id: None, + rated: true, }, &mut conn, ) @@ -178,6 +186,7 @@ mod tests { creators: Vec::new(), video: None, level_id: Some(-1), + rated: true, }, &mut conn, ) diff --git a/pointercrate-demonlist/src/nationality/get.rs b/pointercrate-demonlist/src/nationality/get.rs index 87328ec6..1f49d5e7 100644 --- a/pointercrate-demonlist/src/nationality/get.rs +++ b/pointercrate-demonlist/src/nationality/get.rs @@ -127,8 +127,8 @@ impl Nationality { pub async fn unbeaten_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { let mut stream = sqlx::query!( - r#"select name::text as "name!", id as "id!", position as "position!" from demons where position <= $1 except (select demons.name, demons.id, position from records inner join players on - players.id=records.player inner join demons on demons.id=records.demon where status_='APPROVED' and nationality=$2 and progress=100 union select demons.name, demons.id, demons.position from demons inner join players on players.id=verifier where players.nationality=$2)"#, + r#"select name::text as "name!", id as "id!", position as "position!", rated_position from demons where position <= $1 except (select demons.name, demons.id, position, rated_position from records inner join players on + players.id=records.player inner join demons on demons.id=records.demon where status_='APPROVED' and nationality=$2 and progress=100 union select demons.name, demons.id, demons.position, demons.rated_position from demons inner join players on players.id=verifier where players.nationality=$2)"#, crate::config::extended_list_size(), nation.iso_country_code ) @@ -142,6 +142,7 @@ pub async fn unbeaten_in(nation: &Nationality, connection: &mut PgConnection) -> unbeaten.push(MinimalDemon { id: row.id, position: row.position, + rated_position: row.rated_position, name: row.name, }); } @@ -150,7 +151,7 @@ pub async fn unbeaten_in(nation: &Nationality, connection: &mut PgConnection) -> } pub async fn created_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { - let mut stream = sqlx::query!( r#"select demon, demons.name::text as "demon_name!", demons.position, players.name::text as "player_name!" from creators inner join demons on demons.id=demon inner join players on players.id=creator where nationality=$1 order by demon"#, nation.iso_country_code).fetch(connection); + let mut stream = sqlx::query!( r#"select demon, demons.name::text as "demon_name!", demons.position, demons.rated_position, players.name::text as "player_name!" from creators inner join demons on demons.id=demon inner join players on players.id=creator where nationality=$1 order by demon"#, nation.iso_country_code).fetch(connection); let mut creations = Vec::::new(); @@ -164,6 +165,7 @@ pub async fn created_in(nation: &Nationality, connection: &mut PgConnection) -> id: row.demon, name: row.demon_name, position: row.position, + rated_position: row.rated_position, }, players: vec![row.player_name], }), @@ -175,7 +177,7 @@ pub async fn created_in(nation: &Nationality, connection: &mut PgConnection) -> pub async fn verified_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { let mut stream = sqlx::query!( - r#"select demons.id as demon, demons.name::text as "demon_name!", demons.position, players.name::text as "player_name!" from demons inner join players on players.id=verifier where nationality=$1"#, nation.iso_country_code).fetch(connection); + r#"select demons.id as demon, demons.name::text as "demon_name!", demons.position, demons.rated_position, players.name::text as "player_name!" from demons inner join players on players.id=verifier where nationality=$1"#, nation.iso_country_code).fetch(connection); let mut demons = Vec::new(); @@ -187,6 +189,7 @@ pub async fn verified_in(nation: &Nationality, connection: &mut PgConnection) -> id: row.demon, name: row.demon_name, position: row.position, + rated_position: row.rated_position, }, players: vec![row.player_name], }); @@ -197,7 +200,7 @@ pub async fn verified_in(nation: &Nationality, connection: &mut PgConnection) -> pub async fn published_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { let mut stream = sqlx::query!( - r#"select demons.id as demon, demons.name::text as "demon_name!", demons.position, players.name::text as "player_name!" from demons inner join players on players.id=publisher where nationality=$1"#, nation.iso_country_code).fetch(connection); + r#"select demons.id as demon, demons.name::text as "demon_name!", demons.position, demons.rated_position, players.name::text as "player_name!" from demons inner join players on players.id=publisher where nationality=$1"#, nation.iso_country_code).fetch(connection); let mut demons = Vec::new(); @@ -209,6 +212,7 @@ pub async fn published_in(nation: &Nationality, connection: &mut PgConnection) - id: row.demon, name: row.demon_name, position: row.position, + rated_position: row.rated_position, }, players: vec![row.player_name], }); @@ -219,7 +223,7 @@ pub async fn published_in(nation: &Nationality, connection: &mut PgConnection) - pub async fn best_records_in(nation: &Nationality, connection: &mut PgConnection) -> Result> { let mut stream = sqlx::query!( - r#"SELECT progress as "progress!", demons.id AS "demon_id!", demons.name as "demon_name!: String", demons.position as "position!", players.name as "player_name!: String" FROM best_records_in($1) as records INNER JOIN demons ON records.demon = demons.id INNER JOIN players ON players.id = records.player"#, + r#"SELECT progress as "progress!", demons.id AS "demon_id!", demons.name as "demon_name!: String", demons.position as "position!", demons.rated_position, players.name as "player_name!: String" FROM best_records_in($1) as records INNER JOIN demons ON records.demon = demons.id INNER JOIN players ON players.id = records.player"#, nation.iso_country_code ) .fetch(connection); @@ -236,6 +240,7 @@ pub async fn best_records_in(nation: &Nationality, connection: &mut PgConnection id: row.demon_id, name: row.demon_name, position: row.position, + rated_position: row.rated_position, }, progress: row.progress, players: vec![row.player_name], diff --git a/pointercrate-demonlist/src/nationality/mod.rs b/pointercrate-demonlist/src/nationality/mod.rs index fdf35854..9c1594d8 100644 --- a/pointercrate-demonlist/src/nationality/mod.rs +++ b/pointercrate-demonlist/src/nationality/mod.rs @@ -125,14 +125,14 @@ impl Nationality { /// Updates the score for this [`Nationality`] and contained [`Subdivision`] (if set). pub async fn update_nation_score(&self, connection: &mut PgConnection) -> Result<(), sqlx::Error> { sqlx::query!( - "UPDATE nationalities SET score = coalesce(score_of_nation($1), 0) WHERE iso_country_code = $1", + "UPDATE nationalities SET score = coalesce(score_of_nation(true, $1), 0), unrated_score = coalesce(score_of_nation(false, $1), 0) WHERE iso_country_code = $1", self.iso_country_code ) .execute(&mut *connection) .await?; if let Some(ref subdivision) = self.subdivision { sqlx::query!( - "UPDATE subdivisions SET score = coalesce(score_of_subdivision($1, $2), 0) WHERE nation = $1 AND iso_code = $2", + "UPDATE subdivisions SET score = coalesce(score_of_subdivision(true, $1, $2), 0), unrated_score = coalesce(score_of_subdivision(false, $1, $2), 0) WHERE nation = $1 AND iso_code = $2", self.iso_country_code, subdivision.iso_code ) diff --git a/pointercrate-demonlist/src/record/get.rs b/pointercrate-demonlist/src/record/get.rs index a5bcb155..d22b735c 100644 --- a/pointercrate-demonlist/src/record/get.rs +++ b/pointercrate-demonlist/src/record/get.rs @@ -21,6 +21,7 @@ struct FetchedRecord { demon_id: i32, demon_name: String, position: i16, + rated_position: Option, submitter_id: i32, submitter_banned: bool, } @@ -46,6 +47,7 @@ impl FullRecord { demon: MinimalDemon { id: row.demon_id, position: row.position, + rated_position: row.rated_position, name: row.demon_name, }, submitter: Some(Submitter { @@ -63,7 +65,7 @@ impl FullRecord { pub async fn approved_records_by(player: &DatabasePlayer, connection: &mut PgConnection) -> Result> { let mut stream = sqlx::query!( r#"SELECT records.id, progress, CASE WHEN players.link_banned THEN NULL ELSE records.video::text END, demons.id AS demon_id, - demons.name, demons.position FROM records INNER JOIN demons ON records.demon = demons.id INNER JOIN players ON players.id + demons.name, demons.position, demons.rated_position FROM records INNER JOIN demons ON records.demon = demons.id INNER JOIN players ON players.id = $1 WHERE status_ = 'APPROVED' AND records.player = $1"#, player.id ) @@ -82,6 +84,7 @@ pub async fn approved_records_by(player: &DatabasePlayer, connection: &mut PgCon demon: MinimalDemon { id: row.demon_id, position: row.position, + rated_position: row.rated_position, name: row.name, }, }) diff --git a/pointercrate-demonlist/src/record/paginate.rs b/pointercrate-demonlist/src/record/paginate.rs index f58fce1e..9dff9442 100644 --- a/pointercrate-demonlist/src/record/paginate.rs +++ b/pointercrate-demonlist/src/record/paginate.rs @@ -37,6 +37,16 @@ pub struct RecordPagination { #[serde(rename = "demon_position__gt")] demon_position_gt: Option, + demon_rated_position: Option, + + #[serde(default, deserialize_with = "non_nullable")] + #[serde(rename = "demon_rated_position__lt")] + demon_rated_position_lt: Option, + + #[serde(default, deserialize_with = "non_nullable")] + #[serde(rename = "demon_rated_position__gt")] + demon_rated_position_gt: Option, + #[serde(default, deserialize_with = "non_nullable")] pub status: Option, @@ -94,6 +104,9 @@ impl Paginatable for MinimalRecordPD { .bind(query.player) .bind(query.submitter) .bind(query.params.limit + 1) + .bind(query.demon_rated_position) + .bind(query.demon_rated_position_lt) + .bind(query.demon_rated_position_gt) .fetch(&mut *connection); let mut records = Vec::new(); @@ -114,6 +127,7 @@ impl Paginatable for MinimalRecordPD { demon: MinimalDemon { id: row.try_get("demon_id")?, position: row.try_get("position")?, + rated_position: row.try_get("rated_position")?, name: row.try_get("demon_name")?, }, }) From 63e572ec719014b23cfb37265372200349aca935 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:15:18 -0400 Subject: [PATCH 04/19] list enum --- pointercrate-demonlist/src/lib.rs | 1 + pointercrate-demonlist/src/list.rs | 77 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 pointercrate-demonlist/src/list.rs diff --git a/pointercrate-demonlist/src/lib.rs b/pointercrate-demonlist/src/lib.rs index a95f99bb..7756734d 100644 --- a/pointercrate-demonlist/src/lib.rs +++ b/pointercrate-demonlist/src/lib.rs @@ -6,6 +6,7 @@ pub mod demon; pub mod config; pub mod creator; pub mod error; +pub mod list; pub mod nationality; pub mod player; pub mod record; diff --git a/pointercrate-demonlist/src/list.rs b/pointercrate-demonlist/src/list.rs new file mode 100644 index 00000000..cc07ba72 --- /dev/null +++ b/pointercrate-demonlist/src/list.rs @@ -0,0 +1,77 @@ +use std::str::FromStr; + +use pointercrate_core::error::CoreError; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Clone, Copy)] +pub enum List { + Demonlist, // only consists of rated demons (Demonlist) + RatedPlus, // consists of ALL demons (Rated+ List) +} + +impl List { + pub fn as_str(&self) -> &'static str { + match self { + List::Demonlist => "demonlist", + List::RatedPlus => "ratedplus", + } + } + + pub fn to_key(&self) -> &'static str { + match self { + List::Demonlist => "list-demonlist", + List::RatedPlus => "list-ratedplus", + } + } +} + +impl Default for List { + fn default() -> Self { + List::Demonlist + } +} + +impl From<&str> for List { + fn from(value: &str) -> Self { + List::from_str(value).unwrap_or_default() + } +} + +impl From> for List { + fn from(value: Option<&str>) -> Self { + match value { + Some(list) => List::from(list), + None => List::default(), + } + } +} + +impl FromStr for List { + type Err = CoreError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "demonlist" => Ok(List::Demonlist), + "ratedplus" => Ok(List::RatedPlus), + _ => Err(CoreError::UnprocessableEntity), + } + } +} + +impl ToString for List { + fn to_string(&self) -> String { + self.as_str().to_string() + } +} + +impl<'de> Deserialize<'de> for List { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + + List::from_str(&string[..]) + .map_err(|_| serde::de::Error::invalid_value(serde::de::Unexpected::Str(&string), &"'demonlist', 'ratedplus'")) + } +} From fe541bf3460c47e59db7ef8505b5203cbde6c951 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:22:48 -0400 Subject: [PATCH 05/19] updated stats viewer queries & logic to account for different lists --- .../src/statsviewer/individual.rs | 18 ++++--- .../src/statsviewer/mod.rs | 32 ++++++++--- .../src/statsviewer/national.rs | 22 ++++---- .../static/js/modules/statsviewer.js | 18 ++++--- .../static/js/statsviewer/individual.js | 51 ++++++++++-------- .../static/js/statsviewer/nation.js | 53 +++++++++++-------- pointercrate-demonlist/src/player/get.rs | 8 +-- pointercrate-demonlist/src/player/mod.rs | 15 +++--- pointercrate-demonlist/src/player/paginate.rs | 28 ++++++---- pointercrate-demonlist/src/player/patch.rs | 4 +- 10 files changed, 153 insertions(+), 96 deletions(-) diff --git a/pointercrate-demonlist-pages/src/statsviewer/individual.rs b/pointercrate-demonlist-pages/src/statsviewer/individual.rs index bd2cc2a5..9053d7b8 100644 --- a/pointercrate-demonlist-pages/src/statsviewer/individual.rs +++ b/pointercrate-demonlist-pages/src/statsviewer/individual.rs @@ -1,12 +1,13 @@ use crate::statsviewer::stats_viewer_html; -use maud::{html, Markup}; +use maud::{html, Markup, PreEscaped}; use pointercrate_core::localization::tr; use pointercrate_core_pages::{head::HeadLike, trp_html, PageFragment}; -use pointercrate_demonlist::nationality::Nationality; +use pointercrate_demonlist::{list::List, nationality::Nationality}; #[derive(Debug)] pub struct IndividualStatsViewer { pub nationalities_in_use: Vec, + pub list: List, } impl From for PageFragment { @@ -20,6 +21,9 @@ impl From for PageFragment { .module("/static/demonlist/js/statsviewer/individual.js") .stylesheet("/static/demonlist/css/statsviewer.css") .stylesheet("/static/core/css/sidebar.css") + .head(html! { + (PreEscaped(format!(r#""#, &stats_viewer.list.as_str()))) + }) .body(stats_viewer.body()) } } @@ -28,19 +32,17 @@ impl IndividualStatsViewer { fn body(&self) -> Markup { html! { nav.flex.wrap.m-center.fade #statsviewers style="text-align: center; z-index: 1" { - a.button.white.hover.no-shadow href="/demonlist/statsviewer/"{ + a.button.white.hover.no-shadow href=(format!("/{}/statsviewer/", &self.list.as_str())){ b {(tr("statsviewer-individual"))} } - a.button.white.hover.no-shadow href="/demonlist/statsviewer/nations/" { + a.button.white.hover.no-shadow href=(format!("/{}/statsviewer/nations/", &self.list.as_str())) { b {(tr("statsviewer-nation"))} } } - div #world-map-wrapper { - object style="min-width:100%" #world-map data="/static/demonlist/images/world.svg" type="image/svg+xml" alt="World map showing the global demonlist score distribution" {} - } + (super::world_map()) div.flex.m-center.container { main.left { - (stats_viewer_html(Some(&self.nationalities_in_use), super::standard_stats_viewer_rows(), false)) + (stats_viewer_html(Some(&self.nationalities_in_use), super::standard_stats_viewer_rows(&self.list), false)) } aside.right { (super::demon_sorting_panel()) diff --git a/pointercrate-demonlist-pages/src/statsviewer/mod.rs b/pointercrate-demonlist-pages/src/statsviewer/mod.rs index cefd8133..1c8a3474 100644 --- a/pointercrate-demonlist-pages/src/statsviewer/mod.rs +++ b/pointercrate-demonlist-pages/src/statsviewer/mod.rs @@ -1,12 +1,12 @@ use maud::{html, Markup, PreEscaped}; -use pointercrate_core::localization::tr; +use pointercrate_core::{localization::tr, trp}; use pointercrate_core_pages::util::{dropdown, filtered_paginator, simple_dropdown}; -use pointercrate_demonlist::nationality::Nationality; +use pointercrate_demonlist::{list::List, nationality::Nationality}; pub mod individual; pub mod national; -pub(crate) fn stats_viewer_panel() -> Markup { +pub(crate) fn stats_viewer_panel(list: &List) -> Markup { html! { section #stats.panel.fade.js-scroll-anim data-anim = "fade" { div.underlined { @@ -17,13 +17,25 @@ pub(crate) fn stats_viewer_panel() -> Markup { p { (tr("statsviewer-panel.info")) } - a.blue.hover.button #show-stats-viewer href = "/demonlist/statsviewer/ "{ + a.blue.hover.button #show-stats-viewer href = (format!("/{}/statsviewer/", list.as_str())) { (tr("statsviewer-panel.button")) } } } } +fn world_map() -> Markup { + let map = include_str!("../../static/images/world.svg").to_string(); + + html! { + div #world-map-wrapper { + div #world-map style="min-width:100%" { + (PreEscaped(map)) + } + } + } +} + fn continent_panel() -> Markup { html! { section.panel.fade style="overflow:initial"{ @@ -86,10 +98,16 @@ fn hide_subdivision_panel() -> Markup { struct StatsViewerRow(Vec<(String, &'static str)>); -fn standard_stats_viewer_rows() -> Vec { +fn standard_stats_viewer_rows(list: &List) -> Vec { vec![ - StatsViewerRow(vec![(tr("statsviewer.rank"), "rank"), (tr("statsviewer.score"), "score")]), - StatsViewerRow(vec![(tr("statsviewer.stats"), "stats"), (tr("statsviewer.hardest"), "hardest")]), + StatsViewerRow(vec![ + (trp!("statsviewer.rank", "list" = tr(list.to_key())), "rank"), + (trp!("statsviewer.score", "list" = tr(list.to_key())), "score"), + ]), + StatsViewerRow(vec![ + (trp!("statsviewer.stats", "list" = tr(list.to_key())), "stats"), + (tr("statsviewer.hardest"), "hardest"), + ]), StatsViewerRow(vec![(tr("statsviewer.completed"), "beaten")]), StatsViewerRow(vec![(tr("statsviewer.completed-main"), "main-beaten")]), StatsViewerRow(vec![(tr("statsviewer.completed-extended"), "extended-beaten")]), diff --git a/pointercrate-demonlist-pages/src/statsviewer/national.rs b/pointercrate-demonlist-pages/src/statsviewer/national.rs index 1ec2debd..fad2bf7b 100644 --- a/pointercrate-demonlist-pages/src/statsviewer/national.rs +++ b/pointercrate-demonlist-pages/src/statsviewer/national.rs @@ -1,9 +1,10 @@ use crate::statsviewer::{stats_viewer_html, StatsViewerRow}; -use maud::{html, Markup}; +use maud::{html, Markup, PreEscaped}; use pointercrate_core::localization::tr; use pointercrate_core_pages::{head::HeadLike, PageFragment}; +use pointercrate_demonlist::list::List; -pub fn nation_based_stats_viewer() -> PageFragment { +pub fn nation_based_stats_viewer(list: &List) -> PageFragment { PageFragment::new( "Nation Stats Viewer", "The pointercrate nation stats viewer, ranking how well each nation's players are doing in their quest to collectively complete \ @@ -13,27 +14,28 @@ pub fn nation_based_stats_viewer() -> PageFragment { .module("/static/demonlist/js/statsviewer/nation.js") .stylesheet("/static/demonlist/css/statsviewer.css") .stylesheet("/static/core/css/sidebar.css") - .body(nation_based_stats_viewer_html()) + .head(html! { + (PreEscaped(format!(r#""#, &list.as_str()))) + }) + .body(nation_based_stats_viewer_html(list)) } -fn nation_based_stats_viewer_html() -> Markup { - let mut rows = super::standard_stats_viewer_rows(); +fn nation_based_stats_viewer_html(list: &List) -> Markup { + let mut rows = super::standard_stats_viewer_rows(list); rows[0].0.insert(1, (tr("statsviewer-nation.players"), "players")); rows.push(StatsViewerRow(vec![(tr("statsviewer-nation.unbeaten"), "unbeaten")])); html! { nav.flex.wrap.m-center.fade #statsviewers style="text-align: center; z-index: 1" { - a.button.white.hover.no-shadow href="/demonlist/statsviewer/"{ + a.button.white.hover.no-shadow href=(format!("/{}/statsviewer/", list.as_str())) { b {(tr("statsviewer-individual"))} } - a.button.white.hover.no-shadow href="/demonlist/statsviewer/nations/" { + a.button.white.hover.no-shadow href=(format!("/{}/statsviewer/nations/", list.as_str())) { b {(tr("statsviewer-nation"))} } } - div #world-map-wrapper { - object style="min-width:100%" #world-map data="/static/demonlist/images/world.svg" type="image/svg+xml" alt="World map showing the global demonlist score distribution" {} - } + (super::world_map()) div.flex.m-center.container { main.left { (stats_viewer_html(None, rows, true)) diff --git a/pointercrate-demonlist-pages/static/js/modules/statsviewer.js b/pointercrate-demonlist-pages/static/js/modules/statsviewer.js index b080d05c..1f2cade9 100644 --- a/pointercrate-demonlist-pages/static/js/modules/statsviewer.js +++ b/pointercrate-demonlist-pages/static/js/modules/statsviewer.js @@ -66,6 +66,8 @@ export class StatsViewer extends FilteredPaginator { }); } + this.updateQueryData("list", window.active_list); + this.demonSortingModeDropdown = new Dropdown( document.getElementById("demon-sorting-mode-dropdown") ); @@ -163,12 +165,14 @@ export class StatsViewer extends FilteredPaginator { formatDemon(demon, link, dontStyle) { var element; + const demonPositionKey = window.active_list == "demonlist" ? "rated_position" : "position"; + if (dontStyle) { element = document.createElement("span"); } else { - if (demon.position <= this.list_size) { + if (demon[demonPositionKey] <= this.list_size) { element = document.createElement("b"); - } else if (demon.position <= this.extended_list_size) { + } else if (demon[demonPositionKey] <= this.extended_list_size) { element = document.createElement("span"); } else { element = document.createElement("i"); @@ -241,7 +245,7 @@ export class InteractiveWorldMap { constructor() { this.wrapper = document.getElementById("world-map-wrapper"); this.map = document.getElementById("world-map"); - this.svg = this.map.contentDocument.children[0]; + this.svg = this.map.children[0]; this.selectionListeners = []; this.deselectionListeners = []; @@ -260,7 +264,7 @@ export class InteractiveWorldMap { this.currentlySelected = undefined; - for (let subdivision of this.map.contentDocument.querySelectorAll( + for (let subdivision of this.map.querySelectorAll( ".land-with-states .state" )) { subdivision.addEventListener("click", (event) => { @@ -283,7 +287,7 @@ export class InteractiveWorldMap { }); } - for (let clickable of this.map.contentDocument.querySelectorAll( + for (let clickable of this.map.querySelectorAll( ".land, .island, .land-with-states" )) { clickable.addEventListener("click", () => { @@ -363,7 +367,7 @@ export class InteractiveWorldMap { } showSubdivisions() { - for (let divided of this.map.contentDocument.querySelectorAll( + for (let divided of this.map.querySelectorAll( ".land-with-states" )) { divided.classList.add("subdivided"); @@ -371,7 +375,7 @@ export class InteractiveWorldMap { } hideSubdivisions() { - for (let divided of this.map.contentDocument.querySelectorAll( + for (let divided of this.map.querySelectorAll( ".land-with-states.subdivided" )) { divided.classList.remove("subdivided"); diff --git a/pointercrate-demonlist-pages/static/js/statsviewer/individual.js b/pointercrate-demonlist-pages/static/js/statsviewer/individual.js index 51a5da4e..9cf85e09 100644 --- a/pointercrate-demonlist-pages/static/js/statsviewer/individual.js +++ b/pointercrate-demonlist-pages/static/js/statsviewer/individual.js @@ -24,27 +24,36 @@ class IndividualStatsViewer extends StatsViewer { var playerData = response.data.data; - this._rank.innerText = playerData.rank || "-"; - this._score.innerText = playerData.score.toFixed(2); + const rankKey = window.active_list == "demonlist" ? "rated_rank" : "rank"; + const scoreKey = window.active_list == "demonlist" ? "rated_score" : "score"; + // this doesn't need to be used in sorting operations because position and rated_position will always be in the same order (and position is non-nullable) + const demonPositionKey = window.active_list == "demonlist" ? "rated_position" : "position"; + + this._rank.innerText = playerData[rankKey] || "-"; + this._score.innerText = playerData[scoreKey].toFixed(2); this.setName(playerData.name, playerData.nationality); const selectedSort = this.demonSortingModeDropdown.selected; + let created = playerData.created.filter((demon) => demon[demonPositionKey] != null); + let published = playerData.published.filter((demon) => demon[demonPositionKey] != null); + let verified = playerData.verified.filter((demon) => demon[demonPositionKey] != null); + this.formatDemonsInto( this._created, - this.sortStatsViewerRow(selectedSort, playerData.created) + this.sortStatsViewerRow(selectedSort, created) ); this.formatDemonsInto( this._published, - this.sortStatsViewerRow(selectedSort, playerData.published) + this.sortStatsViewerRow(selectedSort, published) ); this.formatDemonsInto( this._verified, - this.sortStatsViewerRow(selectedSort, playerData.verified) + this.sortStatsViewerRow(selectedSort, verified) ); - let beaten = playerData.records.filter((record) => record.progress === 100); + let beaten = playerData.records.filter((record) => record.progress === 100 && record.demon[demonPositionKey] != null); beaten.sort((r1, r2) => r1.demon.name.localeCompare(r2.demon.name)); this.formatRecordsInto(this._beaten, beaten); @@ -52,40 +61,40 @@ class IndividualStatsViewer extends StatsViewer { beaten.sort((r1, r2) => r1.demon.position - r2.demon.position); let legacy = beaten.filter( - (record) => record.demon.position > this.extended_list_size + (record) => record.demon[demonPositionKey] > this.extended_list_size ); let extended = beaten.filter( (record) => - record.demon.position > this.list_size && - record.demon.position <= this.extended_list_size + record.demon[demonPositionKey] > this.list_size && + record.demon[demonPositionKey] <= this.extended_list_size ); let main = beaten.filter( - (record) => record.demon.position <= this.list_size + (record) => record.demon[demonPositionKey] <= this.list_size ); this.formatRecordsInto(this._main_beaten, main, true); this.formatRecordsInto(this._extended_beaten, extended, true); this.formatRecordsInto(this._legacy_beaten, legacy, true); - let verifiedExtended = playerData.verified.filter( + let verifiedExtended = verified.filter( (demon) => - demon.position <= this.extended_list_size && - demon.position > this.list_size + demon[demonPositionKey] <= this.extended_list_size && + demon[demonPositionKey] > this.list_size ).length; - let verifiedLegacy = playerData.verified.filter( - (demon) => demon.position > this.extended_list_size + let verifiedLegacy = verified.filter( + (demon) => demon[demonPositionKey] > this.extended_list_size ).length; this.setCompletionNumber( main.length + - playerData.verified.length - + verified.length - verifiedExtended - verifiedLegacy, extended.length + verifiedExtended, legacy.length + verifiedLegacy ); - let hardest = playerData.verified + let hardest = verified .concat(beaten.map((record) => record.demon)) .reduce((acc, next) => (acc.position > next.position ? next : acc), { name: tr("demonlist", "statsviewer", "statsviewer.value-none"), @@ -99,7 +108,7 @@ class IndividualStatsViewer extends StatsViewer { ); let non100Records = playerData.records.filter( - (record) => record.progress !== 100 + (record) => record.progress !== 100 && record.demon[demonPositionKey] != null ); this.formatRecordsInto( @@ -110,15 +119,15 @@ class IndividualStatsViewer extends StatsViewer { this.demonSortingModeDropdown.addEventListener((selected) => { this.formatDemonsInto( this._created, - this.sortStatsViewerRow(selected, playerData.created) + this.sortStatsViewerRow(selected, created) ); this.formatDemonsInto( this._published, - this.sortStatsViewerRow(selected, playerData.published) + this.sortStatsViewerRow(selected, published) ); this.formatDemonsInto( this._verified, - this.sortStatsViewerRow(selected, playerData.verified) + this.sortStatsViewerRow(selected, verified) ); this.formatRecordsInto( this._progress, diff --git a/pointercrate-demonlist-pages/static/js/statsviewer/nation.js b/pointercrate-demonlist-pages/static/js/statsviewer/nation.js index b1e190bf..a7a9d5b9 100644 --- a/pointercrate-demonlist-pages/static/js/statsviewer/nation.js +++ b/pointercrate-demonlist-pages/static/js/statsviewer/nation.js @@ -22,6 +22,8 @@ class NationStatsViewer extends StatsViewer { onReceive(response) { super.onReceive(response); + const demonPositionKey = window.active_list == "demonlist" ? "rated_position" : "position"; + this._rank.innerText = this.currentlySelected.dataset.rank; this._score.innerHTML = this.currentlySelected.getElementsByTagName("i")[0].innerHTML; @@ -35,6 +37,11 @@ class NationStatsViewer extends StatsViewer { let beaten = []; let progress = []; + let verified = nationData.verified.filter((record) => record.demon[demonPositionKey] != null); + let unbeaten = nationData.unbeaten.filter((demon) => demon[demonPositionKey] != null); + let published = nationData.published.filter((demon) => demon[demonPositionKey] != null); + let created = nationData.created.filter((demon) => demon[demonPositionKey] != null); + let legacy = 0; let extended = 0; @@ -42,11 +49,11 @@ class NationStatsViewer extends StatsViewer { let players = new Set(); - for (let record of nationData.records) { + for (let record of nationData.records.filter((record) => record.demon[demonPositionKey] != null)) { record.players.forEach(players.add, players); if (record.progress !== 100) { - if (!nationData.verified.some((d) => d.demon.id === record.demon.id)) + if (!verified.some((d) => d.demon.id === record.demon.id)) progress.push(record); } else { beaten.push(record); @@ -55,15 +62,15 @@ class NationStatsViewer extends StatsViewer { hardest = record.demon; } - if (record.demon.position > this.list_size) - if (record.demon.position <= this.extended_list_size) ++extended; + if (record.demon[demonPositionKey] > this.list_size) + if (record.demon[demonPositionKey] <= this.extended_list_size) ++extended; else ++legacy; } } let amountBeaten = beaten.length - extended - legacy; - for (let record of nationData.verified) { + for (let record of verified) { record.players.forEach(players.add, players); if (hardest === undefined || record.demon.position < hardest.position) { @@ -71,8 +78,8 @@ class NationStatsViewer extends StatsViewer { } if (!beaten.some((d) => d.demon.id === record.demon.id)) - if (record.demon.position > this.list_size) - if (record.demon.position <= this.extended_list_size) ++extended; + if (record.demon[demonPositionKey] > this.list_size) + if (record.demon[demonPositionKey] <= this.extended_list_size) ++extended; else ++legacy; else ++amountBeaten; } @@ -92,7 +99,7 @@ class NationStatsViewer extends StatsViewer { formatInto( this._main_beaten, beaten - .filter((record) => record.demon.position <= this.list_size) + .filter((record) => record.demon[demonPositionKey] <= this.list_size) .map((record) => this.formatDemonFromRecord(record, true)) ); formatInto( @@ -100,21 +107,21 @@ class NationStatsViewer extends StatsViewer { beaten .filter( (record) => - record.demon.position > this.list_size && - record.demon.position <= this.extended_list_size + record.demon[demonPositionKey] > this.list_size && + record.demon[demonPositionKey] <= this.extended_list_size ) .map((record) => this.formatDemonFromRecord(record, true)) ); formatInto( this._legacy_beaten, beaten - .filter((record) => record.demon.position > this.extended_list_size) + .filter((record) => record.demon[demonPositionKey] > this.extended_list_size) .map((record) => this.formatDemonFromRecord(record, true)) ); formatInto( this._unbeaten, - this.sortStatsViewerRow(selectedSort, nationData.unbeaten).map((demon) => + this.sortStatsViewerRow(selectedSort, unbeaten).map((demon) => this.formatDemon(demon) ) ); @@ -126,7 +133,7 @@ class NationStatsViewer extends StatsViewer { ); formatInto( this._created, - this.sortStatsViewerRow(selectedSort, nationData.created).map( + this.sortStatsViewerRow(selectedSort, created).map( (creation) => { return this.makeTooltip( this.formatDemon(creation.demon), @@ -145,7 +152,7 @@ class NationStatsViewer extends StatsViewer { ); formatInto( this._verified, - this.sortStatsViewerRow(selectedSort, nationData.verified).map( + this.sortStatsViewerRow(selectedSort, verified).map( (verification) => { return this.makeTooltip( this.formatDemon(verification.demon), @@ -161,7 +168,7 @@ class NationStatsViewer extends StatsViewer { ); formatInto( this._published, - this.sortStatsViewerRow(selectedSort, nationData.published).map( + this.sortStatsViewerRow(selectedSort, published).map( (publication) => { return this.makeTooltip( this.formatDemon(publication.demon), @@ -177,10 +184,10 @@ class NationStatsViewer extends StatsViewer { ); this.demonSortingModeDropdown.addEventListener((selected) => { - if (nationData.created.length > 0) { + if (created.length > 0) { formatInto( this._created, - this.sortStatsViewerRow(selected, nationData.created).map( + this.sortStatsViewerRow(selected, created).map( (creation) => { return this.makeTooltip( this.formatDemon(creation.demon), @@ -199,10 +206,10 @@ class NationStatsViewer extends StatsViewer { ); } - if (nationData.published.length > 0) { + if (published.length > 0) { formatInto( this._published, - this.sortStatsViewerRow(selected, nationData.published).map( + this.sortStatsViewerRow(selected, published).map( (publication) => { return this.makeTooltip( this.formatDemon(publication.demon), @@ -217,10 +224,10 @@ class NationStatsViewer extends StatsViewer { ) ); } - if (nationData.verified.length > 0) { + if (verified.length > 0) { formatInto( this._verified, - this.sortStatsViewerRow(selected, nationData.verified).map( + this.sortStatsViewerRow(selected, verified).map( (verification) => { return this.makeTooltip( this.formatDemon(verification.demon), @@ -243,10 +250,10 @@ class NationStatsViewer extends StatsViewer { ) ); - if (nationData.unbeaten.length > 0) + if (unbeaten.length > 0) formatInto( this._unbeaten, - this.sortStatsViewerRow(selected, nationData.unbeaten).map((demon) => + this.sortStatsViewerRow(selected, unbeaten).map((demon) => this.formatDemon(demon) ) ); diff --git a/pointercrate-demonlist/src/player/get.rs b/pointercrate-demonlist/src/player/get.rs index 14ad3677..28dc4aa8 100644 --- a/pointercrate-demonlist/src/player/get.rs +++ b/pointercrate-demonlist/src/player/get.rs @@ -26,7 +26,7 @@ impl Player { pub async fn by_id(id: i32, connection: &mut PgConnection) -> Result { let result = sqlx::query!( - r#"SELECT players.id, players.name, banned, players.score, nationalities.nation::text, iso_country_code::text, iso_code::text as subdivision_code, subdivisions.name::text as subdivision_name, player_ranks.rank FROM players LEFT OUTER JOIN nationalities ON + r#"SELECT players.id, players.name, banned, players.score, players.unrated_score, nationalities.nation::text, iso_country_code::text, iso_code::text as subdivision_code, subdivisions.name::text as subdivision_name, player_ranks.rank, player_ranks.unrated_rank FROM players LEFT OUTER JOIN nationalities ON players.nationality = nationalities.iso_country_code LEFT OUTER JOIN subdivisions ON players.subdivision = subdivisions.iso_code LEFT OUTER JOIN player_ranks ON player_ranks.id = players.id WHERE players.id = $1 AND (subdivisions.nation=nationalities.iso_country_code or players.subdivision is null)"#, id ) @@ -57,8 +57,10 @@ impl Player { name: row.name, banned: row.banned, }, - score: row.score, - rank: row.rank, + rated_score: row.score, + score: row.unrated_score, + rated_rank: row.rank, + rank: row.unrated_rank, nationality, }) }, diff --git a/pointercrate-demonlist/src/player/mod.rs b/pointercrate-demonlist/src/player/mod.rs index d21f7baa..556050e4 100644 --- a/pointercrate-demonlist/src/player/mod.rs +++ b/pointercrate-demonlist/src/player/mod.rs @@ -55,10 +55,13 @@ pub struct Player { /// * Demon movement/addition (recompute all scores) /// * Demon requirement updated (recompute all scores) /// * Demon verifier updated + /// * Demon rate status updated (recompute all scores) /// - Player updates /// * Player banned /// * Player objects merged + pub rated_score: f64, pub score: f64, + pub rated_rank: Option, pub rank: Option, pub nationality: Option, } @@ -83,22 +86,22 @@ impl Taggable for FullPlayer { impl DatabasePlayer { /// Recomputes this player's score and updates it in the database. - pub async fn update_score(&self, connection: &mut PgConnection) -> Result { + pub async fn update_score(&self, connection: &mut PgConnection) -> Result<(f64, f64), CoreError> { // No need to specially handle banned players - they have no approved records, so `score_of_player` will return 0 - let new_score = sqlx::query!( - "UPDATE players SET score = coalesce(score_of_player($1), 0) WHERE id = $1 RETURNING score", + let new_scores = sqlx::query!( + "UPDATE players SET score = coalesce(score_of_player(true, $1), 0), unrated_score = coalesce(score_of_player(false, $1), 0) WHERE id = $1 RETURNING score, unrated_score", self.id ) .fetch_one(&mut *connection) .await?; - sqlx::query!("UPDATE nationalities SET score = coalesce(score_of_nation(nationalities.iso_country_code), 0) FROM players WHERE players.id = $1 AND players.nationality = nationalities.iso_country_code", self.id).execute(&mut *connection).await?; - sqlx::query!("UPDATE subdivisions SET score = coalesce(score_of_subdivision(subdivisions.nation, subdivisions.iso_code), 0) FROM players WHERE players.id = $1 AND players.nationality = subdivisions.nation AND players.subdivision = subdivisions.iso_code", self.id).execute(&mut *connection).await?; + sqlx::query!("UPDATE nationalities SET score = coalesce(score_of_nation(true, nationalities.iso_country_code), 0), unrated_score = coalesce(score_of_nation(false, nationalities.iso_country_code), 0) FROM players WHERE players.id = $1 AND players.nationality = nationalities.iso_country_code", self.id).execute(&mut *connection).await?; + sqlx::query!("UPDATE subdivisions SET score = coalesce(score_of_subdivision(true, subdivisions.nation, subdivisions.iso_code), 0), unrated_score = coalesce(score_of_subdivision(false, subdivisions.nation, subdivisions.iso_code), 0) FROM players WHERE players.id = $1 AND players.nationality = subdivisions.nation AND players.subdivision = subdivisions.iso_code", self.id).execute(&mut *connection).await?; sqlx::query!("REFRESH MATERIALIZED VIEW CONCURRENTLY player_ranks;") .execute(&mut *connection) .await?; - Ok(new_score.score) + Ok((new_scores.score, new_scores.unrated_score)) } } diff --git a/pointercrate-demonlist/src/player/paginate.rs b/pointercrate-demonlist/src/player/paginate.rs index b282bc10..1c58a640 100644 --- a/pointercrate-demonlist/src/player/paginate.rs +++ b/pointercrate-demonlist/src/player/paginate.rs @@ -1,4 +1,5 @@ use crate::{ + list::List, nationality::{Continent, Nationality, Subdivision}, player::{DatabasePlayer, Player}, }; @@ -99,8 +100,10 @@ impl Paginatable for Player { name: row.get("name"), banned: row.get("banned"), }, - score: row.get("score"), - rank: row.get("rank"), + rated_score: row.get("score"), + score: row.get("unrated_score"), + rated_rank: row.get("rank"), + rank: row.get("unrated_rank"), nationality, }) } @@ -118,6 +121,9 @@ pub struct RankingPagination { #[serde(flatten)] pub params: PaginationParameters, + #[serde(default, deserialize_with = "non_nullable")] + list: Option, + #[serde(default, deserialize_with = "nullable")] nation: Option>, @@ -148,8 +154,11 @@ impl PaginationQuery for RankingPagination { pub struct RankedPlayer { #[serde(skip)] index: i64, + score: f64, + rank: Option, #[serde(flatten)] - player: Player, + base: DatabasePlayer, + nationality: Option, } impl Paginatable for RankedPlayer { @@ -164,7 +173,10 @@ impl Paginatable for RankedPlayer { async fn page(query: &RankingPagination, connection: &mut PgConnection) -> Result<(Vec, PageContext), sqlx::Error> { let order = query.params.order(); - let sql_query = format!(include_str!("../../sql/paginate_player_ranking.sql"), order); + let sql_query = match query.list.as_ref().unwrap_or(&List::default()) { + List::Demonlist => format!(include_str!("../../sql/paginate_rated_player_ranking.sql"), order), + List::RatedPlus => format!(include_str!("../../sql/paginate_player_ranking.sql"), order), + }; let mut stream = sqlx::query(&sql_query) .bind(query.params.before) @@ -191,7 +203,8 @@ impl Paginatable for RankedPlayer { _ => None, }; - let player = Player { + players.push(RankedPlayer { + index: row.get("index"), base: DatabasePlayer { id: row.get("id"), name: row.get("name"), @@ -200,11 +213,6 @@ impl Paginatable for RankedPlayer { score: row.get("score"), rank: row.get("rank"), nationality, - }; - - players.push(RankedPlayer { - index: row.get("index"), - player, }) } diff --git a/pointercrate-demonlist/src/player/patch.rs b/pointercrate-demonlist/src/player/patch.rs index 74d4e365..1b2c1376 100644 --- a/pointercrate-demonlist/src/player/patch.rs +++ b/pointercrate-demonlist/src/player/patch.rs @@ -72,7 +72,9 @@ impl FullPlayer { self.set_name(name, connection).await?; } - self.player.score = self.player.base.update_score(connection).await?; + let new_scores = self.player.base.update_score(connection).await?; + self.player.rated_score = new_scores.0; + self.player.score = new_scores.1; Ok(self) } From 74042631d42159b46663164d42bd2344b9b256cf Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:34:49 -0400 Subject: [PATCH 06/19] updated demon pages & page api --- Cargo.lock | 1 + pointercrate-demonlist-api/Cargo.toml | 1 + pointercrate-demonlist-api/src/lib.rs | 3 +- pointercrate-demonlist-api/src/list.rs | 64 +++++++ pointercrate-demonlist-api/src/pages.rs | 158 +++++++++++------- .../src/components/mod.rs | 12 +- .../src/components/submitter.rs | 8 +- .../src/components/time_machine.rs | 10 +- .../src/demon_page.rs | 72 ++++---- pointercrate-demonlist-pages/src/lib.rs | 22 +-- pointercrate-demonlist-pages/src/overview.rs | 61 ++++--- 11 files changed, 270 insertions(+), 142 deletions(-) create mode 100644 pointercrate-demonlist-api/src/list.rs diff --git a/Cargo.lock b/Cargo.lock index aa44177d..47839134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2116,6 +2116,7 @@ dependencies = [ "chrono", "governor", "log", + "maud", "nonzero_ext", "pointercrate-core", "pointercrate-core-api", diff --git a/pointercrate-demonlist-api/Cargo.toml b/pointercrate-demonlist-api/Cargo.toml index b8fe22ff..990874ea 100644 --- a/pointercrate-demonlist-api/Cargo.toml +++ b/pointercrate-demonlist-api/Cargo.toml @@ -26,6 +26,7 @@ chrono = "0.4.41" serde = "1.0.219" governor = "0.10.1" rand = "0.9.2" +maud = "0.27.0" [features] geolocation = ["pointercrate-demonlist-pages/geolocation"] \ No newline at end of file diff --git a/pointercrate-demonlist-api/src/lib.rs b/pointercrate-demonlist-api/src/lib.rs index bd22c2b2..ea583dda 100644 --- a/pointercrate-demonlist-api/src/lib.rs +++ b/pointercrate-demonlist-api/src/lib.rs @@ -8,6 +8,7 @@ pub(crate) mod config; mod endpoints; #[cfg(feature = "geolocation")] mod geolocate; +pub(crate) mod list; pub(crate) mod pages; pub(crate) mod ratelimits; @@ -90,7 +91,7 @@ pub fn setup(rocket: Rocket) -> Rocket { ], ) .mount( - "/demonlist/", + "/", rocket::routes![ pages::overview, pages::stats_viewer, diff --git a/pointercrate-demonlist-api/src/list.rs b/pointercrate-demonlist-api/src/list.rs new file mode 100644 index 00000000..256982a3 --- /dev/null +++ b/pointercrate-demonlist-api/src/list.rs @@ -0,0 +1,64 @@ +use std::str::FromStr; + +use maud::{html, PreEscaped}; +use pointercrate_core::error::CoreError; +use pointercrate_core_pages::PageFragment; +use pointercrate_demonlist::list::List; +use rocket::{ + form::FromFormField, + http::uri::fmt::{FromUriParam, Path, UriDisplay}, + request::FromParam, +}; + +pub struct ClientList(pub List); + +impl<'a> FromParam<'a> for ClientList { + type Error = CoreError; + + fn from_param(param: &'a str) -> Result { + match List::from_str(param) { + Ok(list) => Ok(ClientList(list)), + Err(err) => Err(err), + } + } +} + +impl<'a> FromUriParam for ClientList { + type Target = ClientList; + + fn from_uri_param(param: &'a str) -> Self::Target { + ClientList(List::from(param)) + } +} + +impl UriDisplay for ClientList { + fn fmt(&self, f: &mut rocket::http::uri::fmt::Formatter<'_, Path>) -> std::fmt::Result { + f.write_value(&self.0.as_str()) + } +} + +impl<'a> FromFormField<'a> for ClientList { + fn from_value(field: rocket::form::ValueField<'a>) -> rocket::form::Result<'a, Self> { + Ok(ClientList( + List::from_str(field.value).map_err(|_| rocket::form::Error::validation("Failed to deserialize list"))?, + )) + } +} + +/// A utility method for inserting scripts to pages which are bound to a particular list +/// only used in stats viewer pages, demon pages, and demons overview i think rn +pub fn inject_list_context(list: &List, fragment: impl Into) -> PageFragment { + let fragment: PageFragment = fragment.into(); + let fragment = fragment.head(html! { + (PreEscaped(format!(r#" + + + "#, list.as_str()))) + }); + + fragment +} diff --git a/pointercrate-demonlist-api/src/pages.rs b/pointercrate-demonlist-api/src/pages.rs index 5ce32a87..de65b1d6 100644 --- a/pointercrate-demonlist-api/src/pages.rs +++ b/pointercrate-demonlist-api/src/pages.rs @@ -9,7 +9,6 @@ use pointercrate_core_api::{ error::Result, response::{Page, Response2}, }; -use pointercrate_demonlist::player::claim::PlayerClaim; use pointercrate_demonlist::player::{FullPlayer, Player}; use pointercrate_demonlist::{ demon::{audit::audit_log_for_demon, current_list, list_at, FullDemon, MinimalDemon}, @@ -17,6 +16,7 @@ use pointercrate_demonlist::{ nationality::Nationality, LIST_ADMINISTRATOR, LIST_HELPER, LIST_MODERATOR, }; +use pointercrate_demonlist::{list::List, player::claim::PlayerClaim}; use pointercrate_demonlist_pages::{ components::{team::Team, time_machine::Tardis}, demon_page::{DemonMovement, DemonPage}, @@ -31,10 +31,12 @@ use rand::Rng; use rocket::{futures::StreamExt, http::CookieJar}; use sqlx::PgConnection; +use crate::list::{self, ClientList}; + #[localized] -#[rocket::get("/?&")] +#[rocket::get("//?&")] pub async fn overview( - pool: &State, timemachine: Option, submitter: Option, cookies: &CookieJar<'_>, + pool: &State, timemachine: Option, submitter: Option, list: ClientList, cookies: &CookieJar<'_>, auth: Option>, ) -> Result { // A few months before pointercrate first went live - definitely the oldest data we have @@ -42,7 +44,7 @@ pub async fn overview( let mut connection = pool.connection().await?; - let demonlist = current_list(&mut connection).await?; + let demonlist = current_list(&list.0, &mut connection).await?; let mut specified_when = cookies .get("when") @@ -72,24 +74,28 @@ pub async fn overview( let mut tardis = Tardis::new(timemachine.unwrap_or(false)); if let Some(destination) = specified_when { - let demons_then = list_at(&mut connection, destination.naive_utc()).await?; - tardis.activate(destination, demons_then, !is_april_1st) + let demons_then = list_at(&mut connection, &list.0, destination.naive_utc()).await?; + tardis.activate(list.0.to_owned(), destination, demons_then, !is_april_1st) } - Ok(Page::new(OverviewPage { - team: Team { - admins: User::by_permission(LIST_ADMINISTRATOR, &mut connection).await?, - moderators: User::by_permission(LIST_MODERATOR, &mut connection).await?, - helpers: User::by_permission(LIST_HELPER, &mut connection).await?, - }, - demonlist, - time_machine: tardis, - submitter_initially_visible: submitter.unwrap_or(false), - claimed_player: match auth { - Some(auth) => claimed_full_player(auth.user.user(), &mut connection).await, - None => None, + Ok(Page::new(list::inject_list_context( + &list.0, + OverviewPage { + team: Team { + admins: User::by_permission(LIST_ADMINISTRATOR, &mut connection).await?, + moderators: User::by_permission(LIST_MODERATOR, &mut connection).await?, + helpers: User::by_permission(LIST_HELPER, &mut connection).await?, + }, + list: list.0, + demonlist, + time_machine: tardis, + submitter_initially_visible: submitter.unwrap_or(false), + claimed_player: match auth { + Some(auth) => claimed_full_player(auth.user.user(), &mut connection).await, + None => None, + }, }, - })) + ))) } async fn claimed_full_player(user: &User, connection: &mut PgConnection) -> Option { @@ -99,21 +105,25 @@ async fn claimed_full_player(user: &User, connection: &mut PgConnection) -> Opti player.upgrade(connection).await.ok() } -#[rocket::get("/permalink//")] -pub async fn demon_permalink(demon_id: i32, pool: &State) -> Result { +#[rocket::get("//permalink//")] +pub async fn demon_permalink(demon_id: i32, pool: &State, list: ClientList) -> Result { let mut connection = pool.connection().await?; - let position = MinimalDemon::by_id(demon_id, &mut connection).await?.position; + let demon = MinimalDemon::by_id(demon_id, &mut connection).await?; + + let position = demon.position(&list.0).unwrap_or(demon.position); - Ok(Redirect::to(rocket::uri!("/demonlist", demon_page(position)))) + Ok(Redirect::to(rocket::uri!("/", demon_page(&list.0.as_str(), position)))) } #[localized] -#[rocket::get("//")] -pub async fn demon_page(position: i16, pool: &State, gd: &State) -> Result { +#[rocket::get("///", rank = 2)] +pub async fn demon_page( + position: i16, pool: &State, gd: &State, list: ClientList, +) -> Result { let mut connection = pool.connection().await?; - let full_demon = FullDemon::by_position(position, &mut connection).await?; + let full_demon = FullDemon::by_position(position, &list.0, &mut connection).await?; let audit_log = audit_log_for_demon(full_demon.demon.base.id, &mut connection).await?; @@ -122,7 +132,11 @@ pub async fn demon_page(position: i16, pool: &State, gd: &Stat let mut modifications = audit_log .iter() .filter_map(|entry| match entry.r#type { - AuditLogEntryType::Modification(ref modification) => match modification.position { + AuditLogEntryType::Modification(ref modification) => match if list.0 == List::Demonlist { + modification.rated_position + } else { + modification.position + } { Some(old_position) if old_position > 0 => Some(DemonMovement { from_position: old_position, at: entry.time, @@ -145,54 +159,71 @@ pub async fn demon_page(position: i16, pool: &State, gd: &Stat from_position: modifications .first() .map(|m| m.from_position) - .unwrap_or(full_demon.demon.base.position), + .unwrap_or(full_demon.demon.base.position(&list.0).unwrap()), // cannot panic at: addition, }, ); } - Ok(Page::new(DemonPage { - team: Team { - admins: User::by_permission(LIST_ADMINISTRATOR, &mut connection).await?, - moderators: User::by_permission(LIST_MODERATOR, &mut connection).await?, - helpers: User::by_permission(LIST_HELPER, &mut connection).await?, + Ok(Page::new(list::inject_list_context( + &list.0, + DemonPage { + team: Team { + admins: User::by_permission(LIST_ADMINISTRATOR, &mut connection).await?, + moderators: User::by_permission(LIST_MODERATOR, &mut connection).await?, + helpers: User::by_permission(LIST_HELPER, &mut connection).await?, + }, + demonlist: current_list(&list.0, &mut connection).await?, + list: list.0, + movements: modifications, + integration: gd.load_level_for_demon(&full_demon.demon).await, + data: full_demon, }, - demonlist: current_list(&mut connection).await?, - movements: modifications, - integration: gd.load_level_for_demon(&full_demon.demon).await, - data: full_demon, - })) + ))) } #[localized] -#[rocket::get("/statsviewer/")] -pub async fn stats_viewer(pool: &State) -> Result { +#[rocket::get("//statsviewer/", rank = 1)] +pub async fn stats_viewer(pool: &State, list: ClientList) -> Result { let mut connection = pool.connection().await?; - Ok(Page::new(IndividualStatsViewer { - nationalities_in_use: Nationality::used(&mut connection).await?, - })) + Ok(Page::new(list::inject_list_context( + &list.0, + IndividualStatsViewer { + nationalities_in_use: Nationality::used(&mut connection).await?, + list: list.0, + }, + ))) } #[localized] -#[rocket::get("/statsviewer/nations/")] -pub async fn nation_stats_viewer() -> Page { - Page::new(pointercrate_demonlist_pages::statsviewer::national::nation_based_stats_viewer()) +#[rocket::get("//statsviewer/nations/")] +pub async fn nation_stats_viewer(list: ClientList) -> Page { + Page::new(list::inject_list_context( + &list.0, + pointercrate_demonlist_pages::statsviewer::national::nation_based_stats_viewer(&list.0), + )) } #[localized] -#[rocket::get("/statsviewer/heatmap.css")] -pub async fn heatmap_css(pool: &State) -> Result> { +#[rocket::get("//statsviewer/heatmap.css")] +pub async fn heatmap_css(list: ClientList, pool: &State) -> Result> { let mut connection = pool.connection().await?; let mut css = String::new(); let mut nation_scores = HashMap::new(); - let mut nations_stream = sqlx::query!("SELECT iso_country_code, score FROM nationalities WHERE score > 0.0").fetch(&mut *connection); + let mut nations_stream = sqlx::query!( + r#"SELECT iso_country_code, CASE WHEN $1 THEN score ELSE unrated_score END as score + FROM nationalities + WHERE CASE WHEN $1 THEN score ELSE unrated_score END > 0.0"#, + list.0 == List::Demonlist + ) + .fetch(&mut *connection); while let Some(row) = nations_stream.next().await { let row = row.map_err(DemonlistError::from)?; - nation_scores.insert(row.iso_country_code, row.score); + nation_scores.insert(row.iso_country_code, row.score.unwrap()); } let Some(&max_nation_score) = nation_scores.values().max_by(|a, b| a.total_cmp(b)) else { @@ -201,21 +232,27 @@ pub async fn heatmap_css(pool: &State) -> Result 0.0").fetch(&mut *connection); + let mut subdivisions_stream = sqlx::query!( + r#"SELECT nation, iso_code, CASE WHEN $1 THEN score ELSE unrated_score END as score + FROM subdivisions + WHERE CASE WHEN $1 THEN score ELSE unrated_score END > 0.0"#, + list.0 == List::Demonlist, + ) + .fetch(&mut *connection); while let Some(row) = subdivisions_stream.next().await { let row = row.map_err(DemonlistError::from)?; css.push_str(&make_css_rule( + &list.0, &format!("{}-{}", row.nation, row.iso_code), - row.score, + row.score.unwrap(), *nation_scores.get(&row.nation).unwrap_or(&f64::INFINITY), )) } @@ -223,16 +260,21 @@ pub async fn heatmap_css(pool: &State) -> Result String { +fn make_css_rule(list: &List, code: &str, score: f64, highest_score: f64) -> String { // Artificially adjust the highest score so that score/high_score is never 1. If it were 1, the resulting // color will be equal to the "hover"/"selected" color, which looks bad. let highest_score = highest_score * 1.5; + let target_rgb: [i32; 3] = match list { + List::Demonlist => [0x08, 0x81, 0xc6], + List::RatedPlus => [0xb3, 0x02, 0x05], + }; + format!( ".heatmapped #{0}, .heatmapped #{0} > path {{ fill: rgb({1}, {2}, {3}); }}", code, - 0xda as f64 + (0x08 - 0xda) as f64 * (score / highest_score), - 0xdc as f64 + (0x81 - 0xdc) as f64 * (score / highest_score), - 0xe0 as f64 + (0xc6 - 0xe0) as f64 * (score / highest_score), + 0xda as f64 + (target_rgb[0] - 0xda) as f64 * (score / highest_score), + 0xdc as f64 + (target_rgb[1] - 0xdc) as f64 * (score / highest_score), + 0xe0 as f64 + (target_rgb[2] - 0xe0) as f64 * (score / highest_score), ) } diff --git a/pointercrate-demonlist-pages/src/components/mod.rs b/pointercrate-demonlist-pages/src/components/mod.rs index 3623bc10..599b8da7 100644 --- a/pointercrate-demonlist-pages/src/components/mod.rs +++ b/pointercrate-demonlist-pages/src/components/mod.rs @@ -3,6 +3,7 @@ use maud::{html, Markup, Render}; use pointercrate_core::{localization::tr, trp}; use pointercrate_demonlist::demon::Demon; +use pointercrate_demonlist::list::List; use pointercrate_demonlist::player::DatabasePlayer; pub mod submitter; @@ -17,8 +18,9 @@ pub fn demon_dropdown<'a>(dropdown_id: &str, demons: impl Iterator(pub &'a DatabasePlayer, pub Option<&'static str>); +pub struct P<'a>(pub &'a DatabasePlayer, pub Option<&'static str>, pub &'a List); impl Render for P<'_> { fn render(&self) -> Markup { if let Some(id) = self.1 { html! { - a.underdotted #(id) href = {"/demonlist/statsviewer?player="(self.0.id)} data-id = (self.0.id) target = "_blank" { + a.underdotted #(id) href = {"/" (self.2.as_str()) "/statsviewer?player="(self.0.id)} data-id = (self.0.id) target = "_blank" { (self.0.name) } } } else { html! { - a.underdotted href = {"/demonlist/statsviewer?player="(self.0.id)} data-id = (self.0.id) target = "_blank" { + a.underdotted href = {"/" (self.2.as_str()) "/statsviewer?player="(self.0.id)} data-id = (self.0.id) target = "_blank" { (self.0.name) } } diff --git a/pointercrate-demonlist-pages/src/components/submitter.rs b/pointercrate-demonlist-pages/src/components/submitter.rs index e78525cc..1269d278 100644 --- a/pointercrate-demonlist-pages/src/components/submitter.rs +++ b/pointercrate-demonlist-pages/src/components/submitter.rs @@ -2,16 +2,18 @@ use crate::components::{demon_dropdown, player_selection_dropdown}; use maud::{html, Markup, Render}; use pointercrate_core::{localization::tr, trp}; use pointercrate_core_pages::trp_html; -use pointercrate_demonlist::{config, demon::Demon}; +use pointercrate_demonlist::{config, demon::Demon, list::List}; pub struct RecordSubmitter<'a> { + list: &'a List, initially_visible: bool, demons: &'a [Demon], } impl<'a> RecordSubmitter<'a> { - pub fn new(visible: bool, demons: &'a [Demon]) -> RecordSubmitter<'a> { + pub fn new(list: &'a List, visible: bool, demons: &'a [Demon]) -> RecordSubmitter<'a> { RecordSubmitter { + list, initially_visible: visible, demons, } @@ -36,7 +38,7 @@ impl Render for RecordSubmitter<'_> { (trp!("record-submission.demon-info", "list-size" = config::extended_list_size())) } span.form-input data-type = "dropdown" { - (demon_dropdown("id_demon", self.demons.iter().filter(|demon| demon.base.position <= config::extended_list_size()))) + (demon_dropdown("id_demon", self.demons.iter().filter(|demon| demon.base.position(self.list).unwrap() <= config::extended_list_size()))) p.error {} } h3 { diff --git a/pointercrate-demonlist-pages/src/components/time_machine.rs b/pointercrate-demonlist-pages/src/components/time_machine.rs index b5383bd5..f91406ea 100644 --- a/pointercrate-demonlist-pages/src/components/time_machine.rs +++ b/pointercrate-demonlist-pages/src/components/time_machine.rs @@ -1,10 +1,11 @@ use chrono::{DateTime, Datelike, FixedOffset}; use maud::{html, Markup, Render}; use pointercrate_core::localization::tr; -use pointercrate_demonlist::demon::TimeShiftedDemon; +use pointercrate_demonlist::{demon::TimeShiftedDemon, list::List}; pub enum Tardis { Activated { + list: List, destination: DateTime, demons: Vec, /// Whether the time selection panel should be visible. @@ -23,8 +24,9 @@ impl Tardis { Tardis::Deactivated { show_selector: visible } } - pub fn activate(&mut self, destination: DateTime, demons_then: Vec, show_destination: bool) { + pub fn activate(&mut self, list: List, destination: DateTime, demons_then: Vec, show_destination: bool) { *self = Tardis::Activated { + list, show_selector: self.visible(), demons: demons_then, destination, @@ -49,7 +51,7 @@ impl Render for Tardis { fn render(&self) -> Markup { html! { @match self { - Tardis::Activated { destination, show_destination, ..} if *show_destination => { + Tardis::Activated { list, destination, show_destination, ..} if *show_destination => { div.panel.fade.blue.flex style="align-items: center;" { span style = "text-align: end"{ (tr("time-machine.active-info")) @@ -63,7 +65,7 @@ impl Render for Tardis { } } } - a.white.button href = "/demonlist/" onclick=r#"document.cookie = "when=""# style = "margin-left: 15px"{ b{ (tr("time-machine.return")) }} + a.white.button href = (format!("/{}/", list.as_str())) onclick=r#"document.cookie = "when=""# style = "margin-left: 15px"{ b{ (tr("time-machine.return")) }} } }, _ => {} diff --git a/pointercrate-demonlist-pages/src/demon_page.rs b/pointercrate-demonlist-pages/src/demon_page.rs index 78fc970b..f017abf7 100644 --- a/pointercrate-demonlist-pages/src/demon_page.rs +++ b/pointercrate-demonlist-pages/src/demon_page.rs @@ -10,6 +10,7 @@ use chrono::NaiveDateTime; use maud::{html, Markup, PreEscaped}; use pointercrate_core::{localization::tr, trp}; use pointercrate_core_pages::{head::HeadLike, trp_html, PageFragment}; +use pointercrate_demonlist::list::List; use pointercrate_demonlist::{ config::{self as list_config, extended_list_size}, demon::{Demon, FullDemon}, @@ -25,6 +26,7 @@ pub struct DemonMovement { pub struct DemonPage { pub team: Team, + pub list: List, pub demonlist: Vec, pub data: FullDemon, pub movements: Vec, @@ -53,8 +55,10 @@ impl DemonPage { self.data.demon.base.name // FIXME: flatten the structs, holy shit ); - if self.data.demon.base.position <= extended_list_size() { - title = format!("#{} - {}", self.data.demon.base.position, title); + let position = self.data.demon.base.position(&self.list).unwrap(); + + if position <= extended_list_size() { + title = format!("#{} - {}", position, title); } title @@ -89,36 +93,36 @@ impl DemonPage { "@type": "ListItem", "position": 2, "item": { - "@id": "https://pointercrate.com/demonlist/", - "name": "demonlist" + "@id": "https://pointercrate.com/"##)) (self.list.as_str()) (PreEscaped(format!(r##"/, + "name": "{}"##, self.list.as_str()))) (PreEscaped(r##"/ } },{ "@type": "ListItem", "position": 3, "item": { - "@id": "https://pointercrate.com/demonlist/"##)) (self.data.position()) (PreEscaped(r##"/", + "@id": "https://pointercrate.com/"##)) (self.list.as_str()) (self.data.position()) (PreEscaped(r##"/", "name": ""##)) (self.data.name()) (PreEscaped(r##"" } } ] }, "name": "#"##)) (self.data.position()) " - " (self.data.name()) (PreEscaped(r##"","description": ""##)) (self.description().replace(r"\", r"\\")) (PreEscaped(r##"", - "url": "https://pointercrate.com/demonlist/{0}/" + "url": "https://pointercrate.com/"##)) (self.list.as_str()) (PreEscaped(r##"/{0}/" } "##)) - (PreEscaped(format!(" + (PreEscaped(format!(r#" ", list_config::list_size(), list_config::extended_list_size(), self.data.demon.base.id + "#, list_config::list_size(), list_config::extended_list_size(), self.data.demon.base.id ))) } } fn body(&self) -> Markup { - let dropdowns = super::dropdowns(&self.demonlist.iter().collect::>()[..], Some(&self.data.demon)); + let dropdowns = super::dropdowns(&self.demonlist.iter().collect::>()[..], &self.list, Some(&self.data.demon)); let mut labels = Vec::new(); @@ -150,7 +154,7 @@ impl DemonPage { div.flex.m-center.container { main.left { - (RecordSubmitter::new(false, &self.demonlist)) + (RecordSubmitter::new(&self.list, false, &self.demonlist)) (self.demon_panel()) div.panel.fade.js-scroll-anim.js-collapse data-anim = "fade" { h2.underlined.pad { @@ -187,14 +191,14 @@ impl DemonPage { window.positionChartData = [{},{}]; ", labels.join("','"), - self.movements.iter().map(|movement| movement.from_position.to_string()).collect::>().join(","), self.data.demon.base.position + self.movements.iter().map(|movement| movement.from_position.to_string()).collect::>().join(","), self.data.demon.base.position(&self.list).unwrap() ))) // FIXME: bad } aside.right { (self.team) (super::rules_panel()) (submit_panel()) - (stats_viewer_panel()) + (stats_viewer_panel(&self.list)) (super::discord_panel()) } } @@ -202,24 +206,24 @@ impl DemonPage { } fn demon_panel(&self) -> Markup { - let position = self.data.demon.base.position; + let position = self.data.demon.base.position(&self.list).unwrap(); let name = &self.data.demon.base.name; - let score100 = self.data.demon.score(100); - let score_requirement = self.data.demon.score(self.data.demon.requirement); + let score100 = self.data.demon.score(&self.list, 100); + let score_requirement = self.data.demon.score(&self.list, self.data.demon.requirement); let verified_and_published = html! { @if self.data.demon.publisher == self.data.demon.verifier { (trp_html!( "demon-headline.same-verifier-publisher", - "publisher" = html! {(P(&self.data.demon.publisher, None))} + "publisher" = html! {(P(&self.data.demon.publisher, None, &self.list))} )) } @else { (trp_html!( "demon-headline.unique-verifier-publisher", - "publisher" = html! {(P(&self.data.demon.publisher, None))}, - "verifier" = html! {(P(&self.data.demon.verifier, None))} + "publisher" = html! {(P(&self.data.demon.publisher, None, &self.list))}, + "verifier" = html! {(P(&self.data.demon.verifier, None, &self.list))} )) } }; @@ -228,23 +232,23 @@ impl DemonPage { section.panel.fade.js-scroll-anim data-anim = "fade" { div.underlined { h1 #demon-heading style = "overflow: hidden"{ - @if self.data.demon.base.position != 1 { - a href=(format!("/demonlist/{:?}", self.data.demon.base.position - 1)) { + @if position != 1 { + a href=(format!("/{}/{:?}", self.list.as_str(), position - 1)) { i class="fa fa-chevron-left" style="padding-right: 5%" {} } } (name) @if position as usize != self.demonlist.len() { - a href=(format!("/demonlist/{:?}", position + 1)) { + a href=(format!("/{}/{:?}", self.list.as_str(), position + 1)) { i class="fa fa-chevron-right" style="padding-left: 5%" {} } } } (PreEscaped(format!(r#" - "#, self.data.demon.base.id))) + "#, self.list.as_str(), self.data.demon.base.id))) h3 { @match &self.data.creators[..] { [] => { (trp_html!( @@ -253,42 +257,42 @@ impl DemonPage { )) }, [creator] => { @if creator == &self.data.demon.publisher && creator == &self.data.demon.verifier { - (trp_html!("demon-headline-by", "creator" = html!{(P(creator, None))})) + (trp_html!("demon-headline-by", "creator" = html!{(P(creator, None, &self.list))})) } @else if creator != &self.data.demon.publisher && creator != &self.data.demon.verifier { (trp_html!( "demon-headline.one-creator", - "creator" = html!{(P(creator, None))}, + "creator" = html!{(P(creator, None, &self.list))}, "verified-and-published" = verified_and_published )) } @else if creator == &self.data.demon.publisher { (trp_html!( "demon-headline.one-creator-is-publisher", - "creator" = html!{(P(creator, None))}, - "verifier" = html!{(P(&self.data.demon.verifier, None))} + "creator" = html!{(P(creator, None, &self.list))}, + "verifier" = html!{(P(&self.data.demon.verifier, None, &self.list))} )) } @else { (trp_html!( "demon-headline.one-creator-is-verifier", - "creator" = html!{(P(creator, None))}, - "publisher" = html!{(P(&self.data.demon.publisher, None))} + "creator" = html!{(P(creator, None, &self.list))}, + "publisher" = html!{(P(&self.data.demon.publisher, None, &self.list))} )) } }, [creator1, creator2] => { (trp_html!( "demon-headline.two-creators", - "creator1" = html!{(P(creator1, None))}, - "creator2" = html!{(P(creator2, None))}, + "creator1" = html!{(P(creator1, None, &self.list))}, + "creator2" = html!{(P(creator2, None, &self.list))}, "verified-and-published" = verified_and_published )) }, [creator1, rest @ ..] => { (trp_html!( "demon-headline.more-creators", - "creator" = html!{(P(creator1, None))}, + "creator" = html!{(P(creator1, None, &self.list))}, "more" = html! { div.tooltip.underdotted { (tr("demon-headline.more-creators-tooltip")) @@ -422,7 +426,7 @@ impl DemonPage { } fn records_panel(&self) -> Markup { - let position = self.data.demon.base.position; + let position = self.data.demon.base.position(&self.list).unwrap(); let _name = &self.data.demon.base.name; html! { @@ -482,7 +486,7 @@ impl DemonPage { } } td { - (P(&record.player, None)) + (P(&record.player, None, &self.list)) } td { @if let Some(ref video) = record.video { diff --git a/pointercrate-demonlist-pages/src/lib.rs b/pointercrate-demonlist-pages/src/lib.rs index e2a9cb82..c1b340af 100644 --- a/pointercrate-demonlist-pages/src/lib.rs +++ b/pointercrate-demonlist-pages/src/lib.rs @@ -1,7 +1,7 @@ use maud::{html, Markup}; use pointercrate_core::localization::tr; -use pointercrate_demonlist::{config, demon::Demon}; +use pointercrate_demonlist::{config, demon::Demon, list::List}; pub mod account; pub mod components; @@ -16,7 +16,7 @@ struct ListSection { numbered: bool, } -fn dropdowns(all_demons: &[&Demon], current: Option<&Demon>) -> Markup { +fn dropdowns(all_demons: &[&Demon], list: &List, current: Option<&Demon>) -> Markup { let (main, extended, legacy) = if all_demons.len() < config::list_size() as usize { (all_demons, Default::default(), Default::default()) } else { @@ -35,21 +35,21 @@ fn dropdowns(all_demons: &[&Demon], current: Option<&Demon>) -> Markup { html! { nav.flex.wrap.m-center.fade #lists style="text-align: center;" { // The drop down for the main list: - (dropdown(&ListSection { name: tr("main-list"), description: tr("main-list.info"), id: "mainlist", numbered: true }, main, current)) + (dropdown(&ListSection { name: tr("main-list"), description: tr("main-list.info"), id: "mainlist", numbered: true }, main, list, current)) // The drop down for the extended list: - (dropdown(&ListSection { name: tr("extended-list"), description: tr("extended-list.info"), id: "extended", numbered: true }, extended, current)) + (dropdown(&ListSection { name: tr("extended-list"), description: tr("extended-list.info"), id: "extended", numbered: true }, extended, list, current)) // The drop down for the legacy list: - (dropdown(&ListSection { name: tr("legacy-list"), description: tr("legacy-list.info"), id: "legacy", numbered: false }, legacy, current)) + (dropdown(&ListSection { name: tr("legacy-list"), description: tr("legacy-list.info"), id: "legacy", numbered: false }, legacy, list, current)) } } } -fn dropdown(section: &ListSection, demons: &[&Demon], current: Option<&Demon>) -> Markup { +fn dropdown(section: &ListSection, demons: &[&Demon], list: &List, current: Option<&Demon>) -> Markup { let format = |demon: &Demon| -> Markup { html! { - a href = {"/demonlist/permalink/" (demon.base.id) "/"} { + a href = (format!("/{}/permalink/{}/", list.as_str(), demon.base.id)) { @if section.numbered { - {"#" (demon.base.position) " - " (demon.base.name)} + {"#" (demon.base.position(list).unwrap()) " - " (demon.base.name)} br ; i { (demon.publisher.name) @@ -82,12 +82,12 @@ fn dropdown(section: &ListSection, demons: &[&Demon], current: Option<&Demon>) - ul.flex.wrap.space { @for demon in demons { @match current { - Some(current) if current.base.position == demon.base.position => - li.hover.white.active title={"#" (demon.base.position) " - " (demon.base.name)} { + Some(current) if current.base.position(list) == demon.base.position(list) => + li.hover.white.active title={"#" (demon.base.position(list).unwrap()) " - " (demon.base.name)} { (format(demon)) }, _ => - li.hover.white title={"#" (demon.base.position) " - " (demon.base.name)} { + li.hover.white title={"#" (demon.base.position(list).unwrap()) " - " (demon.base.name)} { (format(demon)) } } diff --git a/pointercrate-demonlist-pages/src/overview.rs b/pointercrate-demonlist-pages/src/overview.rs index 7e425995..dcf54c53 100644 --- a/pointercrate-demonlist-pages/src/overview.rs +++ b/pointercrate-demonlist-pages/src/overview.rs @@ -10,6 +10,7 @@ use crate::{ use maud::{html, Markup, PreEscaped}; use pointercrate_core::{localization::tr, trp}; use pointercrate_core_pages::{head::HeadLike, trp_html, PageFragment}; +use pointercrate_demonlist::list::List; use pointercrate_demonlist::player::FullPlayer; use pointercrate_demonlist::{ config as list_config, config, @@ -18,6 +19,7 @@ use pointercrate_demonlist::{ pub struct OverviewPage { pub team: Team, + pub list: List, pub demonlist: Vec, pub time_machine: Tardis, pub submitter_initially_visible: bool, @@ -72,11 +74,11 @@ impl OverviewPage { } "#)) - (PreEscaped(format!(" + (PreEscaped(format!(r#" ", list_config::list_size(), list_config::extended_list_size()) + window.extended_list_length = {1}; + "#, list_config::list_size(), list_config::extended_list_size()) )) // FIXME: abstract away link ref = "canonical" href = "https://pointercrate.com/demonlist/"; @@ -89,7 +91,7 @@ impl OverviewPage { _ => self.demonlist.iter().collect(), }; - let dropdowns = super::dropdowns(&demons_for_dropdown[..], None); + let dropdowns = super::dropdowns(&demons_for_dropdown[..], &self.list, None); html! { (dropdowns) @@ -97,19 +99,19 @@ impl OverviewPage { div.flex.m-center.container { main.left { (self.time_machine) - (RecordSubmitter::new(self.submitter_initially_visible, &self.demonlist)) + (RecordSubmitter::new(&self.list, self.submitter_initially_visible, &self.demonlist)) @match &self.time_machine { Tardis::Activated { demons, ..} => { @for TimeShiftedDemon {current_demon, position_now} in demons { - @if current_demon.base.position <= list_config::extended_list_size() { + @if current_demon.base.position(&self.list) <= Some(list_config::extended_list_size()) { (self.demon_panel(current_demon, Some(*position_now))) } } }, _ => { @for demon in &self.demonlist { - @if demon.base.position <= list_config::extended_list_size() { + @if demon.base.position(&self.list) <= Some(list_config::extended_list_size()) { (self.demon_panel(demon, None)) } } @@ -121,14 +123,16 @@ impl OverviewPage { (self.team) (super::rules_panel()) (submit_panel()) - (stats_viewer_panel()) + (stats_viewer_panel(&self.list)) (super::discord_panel()) } } } } - fn demon_panel(&self, demon: &Demon, current_position: Option) -> Markup { + fn demon_panel(&self, demon: &Demon, current_position: Option>) -> Markup { + let position = demon.base.position(&self.list).unwrap(); + let video_link = demon.video.as_deref().unwrap_or("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); let progress = self @@ -143,9 +147,9 @@ impl OverviewPage { }) .unwrap_or_default(); - let total_score = format!("{:.2}", demon.score(100)); - let progress_score = format!("{:.2}", demon.score(progress)); - let minimal_score = format!("{:.2}", demon.score(demon.requirement)); + let total_score = format!("{:.2}", demon.score(&self.list, 100)); + let progress_score = format!("{:.2}", demon.score(&self.list, progress)); + let minimal_score = format!("{:.2}", demon.score(&self.list, demon.requirement)); html! { section.panel.fade.flex.mobile-col.completed[progress==100] style="overflow:hidden" { @@ -153,30 +157,35 @@ impl OverviewPage { div.flex.demon-info style = "align-items: center" { div.demon-byline { h2 style = "text-align: left; margin-bottom: 0px" { - a href = {"/demonlist/permalink/" (demon.base.id) "/"} { - "#" (demon.base.position) (PreEscaped(" – ")) (demon.base.name) + a href = {"/" (&self.list.as_str()) "/permalink/" (demon.base.id) "/"} { + "#" (position) (PreEscaped(" – ")) (demon.base.name) } } h3 style = "text-align: left" { (trp_html!( "demon-info", - "publisher" = html!{(P(&demon.publisher, None))} + "publisher" = html!{(P(&demon.publisher, None, &self.list))} )) } div style="text-align: left; font-size: 0.8em" { - @if let Some(current_position) = current_position { - @if current_position > list_config::extended_list_size() { - (tr("time-machine.active-position-legacy")) - } - @else { - (trp!( - "time-machine.active-position", - "position" = current_position - )) - } + @if let Some(maybe_current_position) = current_position { + @if let Some(current_position) = maybe_current_position { + @if current_position > list_config::extended_list_size() { + (tr("time-machine.active-position-legacy")) + } + @else { + (trp!( + "time-machine.active-position", + "position" = current_position + )) + } + } + @else { + (tr("time-machine.active-position-none")) + } } @else { - @if demon.base.position > config::list_size() { + @if demon.base.position(&self.list) > Some(config::list_size()) { (trp!( "demon-info.score-short", "score" = total_score From 84e3397ed536c4cb3248e801068b094717dc045b Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:41:30 -0400 Subject: [PATCH 07/19] api & frontend impl for updated demon audit functionality --- .../src/endpoints/demon.rs | 8 +- .../static/js/demonlist.js | 23 ++- .../static/js/modules/demonlist.js | 2 +- pointercrate-demonlist/src/demon/audit.rs | 144 ++++++++++++++---- 4 files changed, 144 insertions(+), 33 deletions(-) diff --git a/pointercrate-demonlist-api/src/endpoints/demon.rs b/pointercrate-demonlist-api/src/endpoints/demon.rs index 35b25aa8..3f9345f2 100644 --- a/pointercrate-demonlist-api/src/endpoints/demon.rs +++ b/pointercrate-demonlist-api/src/endpoints/demon.rs @@ -1,4 +1,4 @@ -use crate::ratelimits::DemonlistRatelimits; +use crate::{list::ClientList, ratelimits::DemonlistRatelimits}; use pointercrate_core::{audit::AuditLogEntry, pool::PointercratePool}; use pointercrate_core_api::{ error::Result, @@ -57,9 +57,9 @@ pub async fn audit(demon_id: i32, mut auth: Auth) -> Result/audit/movement/")] -pub async fn movement_log(demon_id: i32, pool: &State) -> Result>> { - let log = pointercrate_demonlist::demon::audit::movement_log_for_demon(demon_id, &mut *pool.connection().await?).await?; +#[rocket::get("//audit/movement/?")] +pub async fn movement_log(list: ClientList, demon_id: i32, pool: &State) -> Result>> { + let log = pointercrate_demonlist::demon::audit::movement_log_for_demon(&list.0, demon_id, &mut *pool.connection().await?).await?; if log.is_empty() { return Err(DemonlistError::DemonNotFound { demon_id }.into()); diff --git a/pointercrate-demonlist-pages/static/js/demonlist.js b/pointercrate-demonlist-pages/static/js/demonlist.js index dd79254a..e6c4f728 100644 --- a/pointercrate-demonlist-pages/static/js/demonlist.js +++ b/pointercrate-demonlist-pages/static/js/demonlist.js @@ -16,7 +16,7 @@ $(window).on("load", function () { }); function initializeHistoryTable() { - get("/api/v2/demons/" + window.demon_id + "/audit/movement/").then( + get("/api/v2/demons/" + window.demon_id + "/audit/movement/?list=" + window.active_list).then( (response) => { let data = response.data; let tableBody = document.getElementById("history-table-body"); @@ -39,7 +39,7 @@ function initializeHistoryTable() { let positionChange = entry["new_position"] - lastPosition; - if (lastPosition !== null) { + if (lastPosition !== null && entry["reason"] !== "Unrated") { let arrow = document.createElement("i"); if (positionChange < 0) { @@ -81,6 +81,11 @@ function initializeHistoryTable() { reason = tr("demonlist", "demon", "movements-reason.added"); } else if (entry["reason"] === "Moved") { reason = tr("demonlist", "demon", "movements-reason.moved"); + } else if (entry["reason"] === "Rated") { + reason = tr("demonlist", "demon", "movements-reason.rated"); + } else if (entry["reason"] === "Unrated") { + reason = tr("demonlist", "demon", "movements-reason.unrated"); + newRow.classList.add("moved-down"); } else { if (entry["reason"]["OtherAddedAbove"] !== undefined) { let other = entry["reason"]["OtherAddedAbove"]["other"]; @@ -101,6 +106,20 @@ function initializeHistoryTable() { : trp("demonlist", "demon", "movements-reason.movedabove", { ["demon"]: name, }); + } else if (entry["reason"]["OtherRated"] !== undefined) { + let other = entry["reason"]["OtherRated"]["other"]; + let name = other.name === null ? "A demon" : other["name"]; + + reason = trp("demonlist", "demon", "movements-reason.otherrated", { + ["demon"]: name, + }); + } else if (entry["reason"]["OtherUnrated"] !== undefined) { + let other = entry["reason"]["OtherUnrated"]["other"]; + let name = other.name === null ? "A demon" : other["name"]; + + reason = trp("demonlist", "demon", "movements-reason.otherunrated", { + ["demon"]: name, + }); } } diff --git a/pointercrate-demonlist-pages/static/js/modules/demonlist.js b/pointercrate-demonlist-pages/static/js/modules/demonlist.js index c9d1da26..e2cc59e2 100644 --- a/pointercrate-demonlist-pages/static/js/modules/demonlist.js +++ b/pointercrate-demonlist-pages/static/js/modules/demonlist.js @@ -86,7 +86,7 @@ export function initializeTimeMachine() { document.cookie = "when=" + when; - window.location = "/demonlist/"; + window.location = `/${window.active_list}/`; }); } diff --git a/pointercrate-demonlist/src/demon/audit.rs b/pointercrate-demonlist/src/demon/audit.rs index 44577b7f..32ea3a03 100644 --- a/pointercrate-demonlist/src/demon/audit.rs +++ b/pointercrate-demonlist/src/demon/audit.rs @@ -1,4 +1,5 @@ -use crate::error::Result; +use crate::error::DemonlistError; +use crate::{error::Result, list::List}; use crate::demon::MinimalDemon; use chrono::{NaiveDateTime, NaiveTime}; @@ -8,14 +9,16 @@ use serde::Serialize; use sqlx::PgConnection; use std::collections::HashMap; -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct DemonModificationData { pub name: Option, pub position: Option, + pub rated_position: Option, pub requirement: Option, pub video: Option, pub verifier: Option, pub publisher: Option, + pub rated: Option, } #[derive(Serialize, Debug)] @@ -24,6 +27,10 @@ pub enum MovementReason { Moved, OtherAddedAbove { other: NamedId }, OtherMoved { other: NamedId }, + Rated, + Unrated, + OtherRated { other: NamedId }, + OtherUnrated { other: NamedId }, Unknown, } @@ -36,7 +43,13 @@ pub struct MovementLogEntry { new_position: Option, } -pub async fn movement_log_for_demon(demon_id: i32, connection: &mut PgConnection) -> Result> { +pub async fn movement_log_for_demon(list: &List, demon_id: i32, connection: &mut PgConnection) -> Result> { + // ensure that the demon exists on this list + MinimalDemon::by_id(demon_id, &mut *connection) + .await? + .position(list) + .ok_or(DemonlistError::DemonNotFound { demon_id })?; + let audit_log = audit_log_for_demon(demon_id, connection).await?; let mut movement_log = Vec::new(); @@ -44,6 +57,10 @@ pub async fn movement_log_for_demon(demon_id: i32, connection: &mut PgConnection let mut additions = HashMap::new(); // map time -> NamedId keeping track when movements to -1 happened let mut all_moves = HashMap::new(); + // map time -> NamedId keeping track of all demon rates + let mut rates = HashMap::new(); + // map time -> NamedId keeping track of all demon unrates + let mut unrates = HashMap::new(); { // non-lexical lifetimes working amazingly I see >.> @@ -84,6 +101,27 @@ pub async fn movement_log_for_demon(demon_id: i32, connection: &mut PgConnection } } + { + let mut rate_modification_stream = sqlx::query!( + r#"SELECT time, demon_modifications.id, demon_modifications.rated, demon_modifications.rated_position, demons.name::TEXT FROM demon_modifications + LEFT OUTER JOIN demons ON demons.id = demon_modifications.id WHERE demon_modifications.rated IS NOT NULL"# + ) + .fetch(&mut *connection); + + while let Some(row) = rate_modification_stream.next().await { + let row = row?; + + // add to rates map if this entry sets rated to true, otherwise add to unrates + (if row.rated.unwrap() { &mut unrates } else { &mut rates }).insert( + row.time, + NamedId { + id: row.id, + name: row.name, + }, + ); + } + } + for log_entry in audit_log { let time = log_entry.time; @@ -94,7 +132,30 @@ pub async fn movement_log_for_demon(demon_id: i32, connection: &mut PgConnection reason: MovementReason::Added, }), AuditLogEntryType::Modification(data) => { - if let Some(old_position) = data.position { + if list == &List::Demonlist { + // rates/unrates are only relevant on rated demonlist + if let Some(old_rated) = data.rated { + if old_rated { + movement_log.push(MovementLogEntry { + reason: MovementReason::Unrated, + new_position: None, + time, + }); + } else { + movement_log.push(MovementLogEntry { + reason: MovementReason::Rated, + new_position: None, + time, + }); + } + } + } + + if let Some(old_position) = if list == &List::Demonlist { + data.rated_position + } else { + data.position + } { // whenever a demon is moved, its position is first set to -1, all other demons are shifted, and // then it is moved to its final position however since audit log entries with // the same timestamp are not ordered in any way, trying to use this entry to draw conclusions about @@ -104,7 +165,7 @@ pub async fn movement_log_for_demon(demon_id: i32, connection: &mut PgConnection } // update the previous entry's "new_position" field - if let Some(entry) = movement_log.last_mut() { + if let Some(entry) = movement_log.iter_mut().rev().find(|e| !matches!(e.reason, MovementReason::Unrated)) { entry.new_position = Some(old_position); } @@ -120,35 +181,62 @@ pub async fn movement_log_for_demon(demon_id: i32, connection: &mut PgConnection continue; } - let moved = all_moves.get(&time); + let unrated_demon = unrates.get(&time); - match moved { - Some(id) if id.id == demon_id => movement_log.push(MovementLogEntry { - reason: MovementReason::Moved, - time, + match unrated_demon { + Some(unrated_demon) if unrated_demon.id == demon_id => continue, + Some(unrated_demon) => movement_log.push(MovementLogEntry { + reason: MovementReason::OtherUnrated { + other: unrated_demon.clone(), + }, new_position: None, - }), - Some(id) => movement_log.push(MovementLogEntry { - reason: MovementReason::OtherMoved { other: id.clone() }, - new_position: Some(old_position), time, }), None => { - let added_demon = additions.get(&time); + let moved = all_moves.get(&time); - match added_demon { - Some(added_demon) => movement_log.push(MovementLogEntry { - reason: MovementReason::OtherAddedAbove { - other: added_demon.clone(), - }, - new_position: None, + match moved { + Some(id) if id.id == demon_id => movement_log.push(MovementLogEntry { + reason: MovementReason::Moved, time, - }), - None => movement_log.push(MovementLogEntry { - reason: MovementReason::Unknown, new_position: None, + }), + Some(id) => movement_log.push(MovementLogEntry { + reason: MovementReason::OtherMoved { other: id.clone() }, + new_position: Some(old_position), time, }), + None => { + let added_demon = additions.get(&time); + + match added_demon { + Some(added_demon) => movement_log.push(MovementLogEntry { + reason: MovementReason::OtherAddedAbove { + other: added_demon.clone(), + }, + new_position: None, + time, + }), + None => { + let rated_demon = rates.get(&time); + + match rated_demon { + Some(rated_demon) => movement_log.push(MovementLogEntry { + reason: MovementReason::OtherRated { + other: rated_demon.clone(), + }, + new_position: None, + time, + }), + None => movement_log.push(MovementLogEntry { + reason: MovementReason::Unknown, + new_position: None, + time, + }), + } + }, + } + }, } }, } @@ -172,7 +260,7 @@ pub async fn movement_log_for_demon(demon_id: i32, connection: &mut PgConnection // update the last entry with the current position MinimalDemon::by_id(demon_id, &mut *connection).await.map(|minimal_demon| { if let Some(entry) = movement_log.last_mut() { - entry.new_position = Some(minimal_demon.position) + entry.new_position = Some(minimal_demon.position(&list).unwrap()) } })?; @@ -212,12 +300,14 @@ pub async fn audit_log_for_demon(demon_id: i32, connection: &mut PgConnection) - userid, demon_modifications.name::text, position, + rated_position, requirement, video, verifier, verifiers.name::text as verifier_name, publisher, - publishers.name::text as publisher_name + publishers.name::text as publisher_name, + rated FROM demon_modifications LEFT OUTER JOIN members ON members.member_id = userid LEFT OUTER JOIN players AS verifiers ON verifier=verifiers.id @@ -239,6 +329,7 @@ pub async fn audit_log_for_demon(demon_id: i32, connection: &mut PgConnection) - r#type: AuditLogEntryType::Modification(DemonModificationData { name: row.name, position: row.position, + rated_position: row.rated_position, requirement: row.requirement, video: row.video, verifier: match row.verifier { @@ -255,6 +346,7 @@ pub async fn audit_log_for_demon(demon_id: i32, connection: &mut PgConnection) - }), None => None, }, + rated: row.rated, }), user: NamedId { name: row.username, From 6ec87910b51512eea024f94ae89311b424edb1b7 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:43:05 -0400 Subject: [PATCH 08/19] updated account pages --- .../src/account/demons.rs | 22 +++++++++++++++++++ .../src/account/list_integration.rs | 4 ++-- .../src/account/records.rs | 6 +++-- .../static/js/account/demon.js | 11 ++++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/pointercrate-demonlist-pages/src/account/demons.rs b/pointercrate-demonlist-pages/src/account/demons.rs index dc928a45..e1b5b75d 100644 --- a/pointercrate-demonlist-pages/src/account/demons.rs +++ b/pointercrate-demonlist-pages/src/account/demons.rs @@ -112,6 +112,23 @@ impl AccountPageTab for DemonsTab { span #demon-verifier {} } } + div.stats-container.flex.space { + span { + b { (tr("demon-viewer.rated-field")) } + br; + div.dropdown-menu.js-search #edit-demon-rated style = "max-width: 120px" { + div { + input type = "text" style = "font-weight: bold" {} + } + div.menu { + ul { + li.white.hover data-value="true" { (tr("demon-rated.yes")) } + li.white.hover data-value="false" { (tr("demon-rated.no")) } + } + } + } + } + } div.stats-container.flex.space { span{ i.fa.fa-plus.clickable #demon-add-creator-pen aria-hidden = "true" {} b { @@ -386,6 +403,11 @@ fn demon_submitter() -> Markup { input type = "url" name = "video"; p.error {} } + span.form-input.flex.cb-container.no-stretch #demon-add-rated style = "margin-bottom:20px" { + label for = "rated" { (tr("demon-add-form.rated-field")) } + input type = "checkbox" name = "rated" checked {} + span.checkmark {} + } span { i.fa.fa-plus.clickable #add-demon-add-creator-pen aria-hidden = "true" {} i { " " (tr("demon-add-form.creators-field")) diff --git a/pointercrate-demonlist-pages/src/account/list_integration.rs b/pointercrate-demonlist-pages/src/account/list_integration.rs index 6a939aaf..cb76197e 100644 --- a/pointercrate-demonlist-pages/src/account/list_integration.rs +++ b/pointercrate-demonlist-pages/src/account/list_integration.rs @@ -7,7 +7,7 @@ use pointercrate_core_pages::{ trp_html, util::{filtered_paginator, paginator}, }; -use pointercrate_demonlist::player::claim::PlayerClaim; +use pointercrate_demonlist::{list::List, player::claim::PlayerClaim}; use pointercrate_user::{ auth::{AuthenticatedUser, NonMutating}, MODERATOR, @@ -70,7 +70,7 @@ impl AccountPageTab for ListIntegrationTab { } @match player_claim { Some(ref claim) => { - (P(&claim.player, Some("claimed-player"))) + (P(&claim.player, Some("claimed-player"), &List::RatedPlus)) }, None => { i { diff --git a/pointercrate-demonlist-pages/src/account/records.rs b/pointercrate-demonlist-pages/src/account/records.rs index 64b92ee2..3d999af4 100644 --- a/pointercrate-demonlist-pages/src/account/records.rs +++ b/pointercrate-demonlist-pages/src/account/records.rs @@ -10,6 +10,7 @@ use pointercrate_core_pages::{ }; use pointercrate_demonlist::{ demon::{current_list, Demon}, + list::List, LIST_HELPER, }; use pointercrate_user::auth::{AuthenticatedUser, NonMutating}; @@ -45,7 +46,8 @@ impl AccountPageTab for RecordsPage { async fn content( &self, _user: &AuthenticatedUser, _permissions: &PermissionsManager, connection: &mut PgConnection, ) -> Markup { - let demons = match current_list(connection).await { + // rated+ list consists of ALL demons + let demons = match current_list(&List::RatedPlus, connection).await { Ok(demons) => demons, Err(err) => { return ErrorFragment { @@ -59,7 +61,7 @@ impl AccountPageTab for RecordsPage { html! { div.left { - (RecordSubmitter::new(false, &demons[..])) + (RecordSubmitter::new(&List::RatedPlus, false, &demons[..])) (record_manager(&demons[..])) (note_adder()) div.panel.fade #record-notes-container style = "display:none" { diff --git a/pointercrate-demonlist-pages/static/js/account/demon.js b/pointercrate-demonlist-pages/static/js/account/demon.js index e9ad3918..24875c5e 100644 --- a/pointercrate-demonlist-pages/static/js/account/demon.js +++ b/pointercrate-demonlist-pages/static/js/account/demon.js @@ -20,6 +20,7 @@ import { post, setupEditorDialog, FormDialog, + setupDropdownEditor, } from "/static/core/js/modules/form.js"; import { loadResource, tr } from "/static/core/js/modules/localization.js"; @@ -52,6 +53,14 @@ export class DemonManager extends FilteredPaginator { this._creators = document.getElementById("demon-creators"); + this._rated = setupDropdownEditor( + new PaginatorEditorBackend(this, false), + "edit-demon-rated", + "rated", + this.output, + { true: true, false: false }, + ); + let videoForm = setupFormDialogEditor( new PaginatorEditorBackend(this, false), "demon-video-dialog", @@ -195,6 +204,8 @@ export class DemonManager extends FilteredPaginator { this.currentObject.verifier.id + ")"; + this._rated.selectSilently(this.currentObject.rated.toString()); + while (this._creators.lastChild) { this._creators.removeChild(this._creators.lastChild); } From 5974bfd7ecccfb301eaf9a59556b97a66a5fb801 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:43:39 -0400 Subject: [PATCH 09/19] updated static resources --- .../static/ftl/en-us/demon.ftl | 14 +- .../static/ftl/en-us/error.ftl | 1 + .../static/ftl/en-us/list.ftl | 2 + .../static/ftl/en-us/overview.ftl | 1 + .../static/ftl/en-us/statsviewer.ftl | 6 +- .../static/images/world.svg | 4732 ++++++++++++++++- .../static/ftl/en-us/footer.ftl | 5 + pointercrate-example/static/ftl/en-us/nav.ftl | 5 + 8 files changed, 4699 insertions(+), 67 deletions(-) create mode 100644 pointercrate-demonlist-pages/static/ftl/en-us/list.ftl diff --git a/pointercrate-demonlist-pages/static/ftl/en-us/demon.ftl b/pointercrate-demonlist-pages/static/ftl/en-us/demon.ftl index 250a403c..a76b98f2 100644 --- a/pointercrate-demonlist-pages/static/ftl/en-us/demon.ftl +++ b/pointercrate-demonlist-pages/static/ftl/en-us/demon.ftl @@ -46,6 +46,10 @@ demon-publisher = Publisher demon-verifier = Verifier .validator-valuemissing = Please specify a verifier +demon-rated = Rated + .yes = Yes (Rated) + .no = No (Unrated) + demon-creators = Creators demon-headline-by = by { $creator } @@ -86,6 +90,10 @@ movements-reason = Reason .moved = Moved .movedabove = { $demon } was moved up past this demon .movedbelow = { $demon } was moved down past this demon + .rated = Rated + .unrated = Unrated + .otherrated = { $demon } was rated + .otherunrated = { $demon } was unrated ## Records table demon-records = Records @@ -121,6 +129,7 @@ demon-viewer = Demon # .requirement-field = { demon-requirement }: .publisher-field = { demon-publisher }: .verifier-field = { demon-verifier }: + .rated-field = { demon-rated }: .creators-field = { demon-creators }: demon-add-panel = Add Demon @@ -132,11 +141,12 @@ demon-add-form = Add Demon .name-validator-valuemissing = Please provide a name for the demon .levelid-field = Geometry Dash Level ID: - .position-field = { demon-position }: + .position-field = { demon-position } ({ list-ratedplus }): .requirement-field = { demon-requirement }: .verifier-field = { demon-verifier }: .publisher-field = { demon-publisher }: .video-field = { demon-video }: + .rated-field = { demon-rated } .creators-field = { demon-creators }: .submit = Add Demon @@ -165,7 +175,7 @@ demon-thumbnail-dialog = Change thumbnail link demon-position-dialog = Change demon position .info = Change the position of this demon. Has be be greater than 0 and be at most the current list size. - .position-field = Position: + .position-field = Position ({ list-ratedplus }): .submit = Edit demon-requirement-dialog = Change demon requirement diff --git a/pointercrate-demonlist-pages/static/ftl/en-us/error.ftl b/pointercrate-demonlist-pages/static/ftl/en-us/error.ftl index 6afe63e2..c90061dd 100644 --- a/pointercrate-demonlist-pages/static/ftl/en-us/error.ftl +++ b/pointercrate-demonlist-pages/static/ftl/en-us/error.ftl @@ -14,6 +14,7 @@ error-demonlist-playernotfoundname = No player with name { $player-name } found error-demonlist-demonnotfound = No demon with id { $demon-id } found error-demonlist-demonnotfoundname = No demon with name { $demon-name } found error-demonlist-demonnotfoundposition = No demon at position { $demon-position } found +error-demonlist-listnotfound = This list does not exist error-demonlist-recordnotfound = No record with id { $record-id } found error-demonlist-claimnotfound = No claim by user { $member-id } on player { $player-id } found error-demonlist-creatorexists = This player is already registered as a creator on this demon diff --git a/pointercrate-demonlist-pages/static/ftl/en-us/list.ftl b/pointercrate-demonlist-pages/static/ftl/en-us/list.ftl new file mode 100644 index 00000000..0c114c05 --- /dev/null +++ b/pointercrate-demonlist-pages/static/ftl/en-us/list.ftl @@ -0,0 +1,2 @@ +list-demonlist = Demonlist +list-ratedplus = Rated+ diff --git a/pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl b/pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl index 25a921c6..03ac2e89 100644 --- a/pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl +++ b/pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl @@ -23,6 +23,7 @@ time-machine = Time Machine .active-position = Currently #{ $position } .active-position-legacy = Currently Legacy + .active-position-none = Not currently on this list .active-info = You are currently looking at the demonlist how it was on .return = Go to present diff --git a/pointercrate-demonlist-pages/static/ftl/en-us/statsviewer.ftl b/pointercrate-demonlist-pages/static/ftl/en-us/statsviewer.ftl index 4ab0d4da..e21da171 100644 --- a/pointercrate-demonlist-pages/static/ftl/en-us/statsviewer.ftl +++ b/pointercrate-demonlist-pages/static/ftl/en-us/statsviewer.ftl @@ -1,7 +1,7 @@ statsviewer = Stats Viewer - .rank = Demonlist rank - .score = Demonlist score - .stats = Demonlist stats + .rank = { $list } rank + .score = { $list } score + .stats = { $list } stats .hardest = Hardest demon .completed = Demons completed diff --git a/pointercrate-demonlist-pages/static/images/world.svg b/pointercrate-demonlist-pages/static/images/world.svg index 1577f8b7..a9da8623 100644 --- a/pointercrate-demonlist-pages/static/images/world.svg +++ b/pointercrate-demonlist-pages/static/images/world.svg @@ -1,62 +1,4670 @@ -World MapCubaBonaireJamaicaPuerto RicoDominican RepublicHaitiEl SalvadorGuatemalaHondurasNicaraguaPanamaCosta RicaMexicoJaliscoAguascalientesSan Luis PotosíNuevo LeónTamaulipasCoahuilaQuintana RooYucatánGuerreroCampecheChiapasOaxacaCiudad de MéxicoMorelosPueblaHidalgoMéxicoQueretaroMichoacanGuanajuatoTabascoVeracruzColimaZacatecasDurangoChihuahuaSinaloaBaja CaliforniaSonoraBaja California SurNayaritMontserratBritish Virgin IslandsUS Virgin IslandsSaint Kitts and NevisCayman IslandsAnguillaGrenadaSaint LuciaSaint Vincent and the GrenadinesTurks and Caicos IslandsBarbadosAntigua and BarbudaSint Maarten (Dutch Part)DominicaTrinidad and TobagoBahamasBelizeWashingtonDelawareMarylandWest VirginiaNew YorkNew JerseyPennsylvaniaVirginiaKentuckyOhioIndianaIllinoisMichiganWisconsinConnecticutRhode IslandVermontNew HampshireMassachusettsMaineAlabamaGeorgiaSouth CarolinaFloridaMississippiTennesseeNorth CarolinaTexasOklahomaNew MexicoNebraskaSouth DakotaKansasColoradoNorth DakotaArkansasMissouriLouisianaIowaMinnesotaArizonaNevadaCaliforniaUtahOregonMontanaIdahoWyomingHawaiiAlaskaWashington, District of ColumbiaUnited StatesManitobaNorthwest TerritoriesNewfoundland and LabradorNunavutQuebecBritish ColumbiaSaskatchewanAlbertaOntarioNew BrunswickNova ScotiaPrince Edward IslandYukonCanadaBermudaBrazilSão PauloRio de JaneiroEspírito SantoRio Grande do SulSanta CatarinaParanáMinas GeraisSergipeAlagoasPernambucoParaíbaRio Grande do NorteBahiaCearáPiauíMaranhãoMato Grosso do SulMato GrossoParáAmapáTocantinsAcreRondôniaAmazonasRoraimaGoiásDistrito FederalArica y ParinacotaAntofagastaTarapacáCoquimboO'HigginsBiobíoLos RíosAtacamaRegión Metropolitana de SantiagoValparaísoMauleÑubleAraucaníaAysénLos LagosMagallanesChileArgentinaProvincia de Santa CruzProvincia del ChubutCiudad Autónoma de Buenos AiresProvincia de Buenos AiresProvincia de NeuquénProvincia de Río NegroProvincia de La PampaProvincia de MendozaProvincia de San LuisProvincia de CórdobaProvincia de Santa FeProvincia del ChacoProvincia de FormosaProvincia de La RiojaProvincia de San JuanProvincia de CatamarcaProvincia de Santiago del EsteroProvincia de TucumánProvincia de SaltaProvincia de JujuyProvincia de Entre RíosProvincia de CorrientesProvincia de MisionesProvincia de Tierra del FuegoSurinameGuyanaVenezuela, Bolivarian Republic ofUruguayParaguayEcuadorColombiaDepartamento del AmazonasDepartamento del PutumayoDepartamento de NariñoDepartamento del CaquetáDepartamento de GuainíaDepartamento del VaupésDepartamento del GuaviareDepartamento del MetaDepartamento del VichadaDepartamento de CasanareDepartamento de AraucaDepartamento del CaucaDepartamento del HuilaBogotáDepartamento del TolimaDepartamento de SantanderDepartamento de CórdobaDepartamento de SucreDepartamento del Valle del CaucaDepartamento del QuindíoDepartamento del RisaraldaDepartamento de CaldasDepartamento de AntioquiaDepartamento del ChocóDepartamento de BolívarDepartamento del MagdalenaDepartamento del AtlánticoDepartamento de CundinamarcaDepartamento de BoyacáNorte de SantanderDepartamento del CesarDepartamento de La GuajiraSan Andrés y ProvidenciaPeruMadre de DiosUcayaliTacnaMoqueguaApurímacAyacuchoArequipaIcaHuancavelicaPunoCuzcoCajamarcaSan MartínPiuraTumbesLambayequeJunínPascoHuánucoÁncashDepartment of LimaLa LibertadAmazonasLoretoBolivia, Plurinational State ofArubaSouth Georgia and the South Sandwich IslandsFalkland Islands (Malvinas)CuracaoChadAlgeriaEgyptLibyaMoroccoWestern SaharaSudanTunisiaNigerMauritaniaMaliBurkina FasoEritreaSenegalGambiaGuinea-BissauGuineaSierra LeoneLiberiaCote d'IvoireGhanaTogoBeninCameroonCentral African RepublicSouth SudanEthiopiaDjiboutiSomaliaEquatorial GuineaGabonKenyaTanzania, United Republic ofUgandaBurundiRwandaCongo, the Democratic Republic of theCongoAngolaZambiaMozambiqueMalawiZimbabweNamibiaBotswanaSwazilandLesothoMadagascarNigeriaSouth AfricaSaint Helena, Ascension and Tristan Da CunhaSeychellesCape VerdeSao Tome and PrincipeMauritiusComorosFranceFranceFranceFranceFranceFranceFranceFranceFranceFranceFranceFranceFrancePolynésie FrançaiseWallis-et-FutunaNouvelle-CalédonieSaint-MartinSaint-BarthélemySaint-Pierre-et-MiquelonCorseNouvelle-AquitaineAuvergne-Rhône-AlpesCentre-Val de LoireBourgogne-Franche-ComtéBretagnePays de la LoireGrand EstNormandieÎle-de-FranceHauts-de-FranceProvence-Alpes-Côte d'AzurOccitanieLa RéunionMayotteMartiniqueGuadeloupeTerres Australes et Antarctiques FrançaisesGuyaneFranceSloveniaSerbiaKosovoMontenegroMacedonia, the Former Yugoslav Republic ofGreeceCroatiaBosnia and HerzegovinaAlbaniaHoly See (Vatican City State)San MarinoPugliaMoliseAbruzzoMarcheEmilia-RomagnaLombardiaLazioUmbriaVenetoTrentino-Alto AdigeBasilicataCampaniaToscanaLiguriaPiemonteCalabriaFriuli-Venezia GiuliaValle d'AostaSiciliaSardegnaItalySlovakiaRomaniaWojewództwo warmińsko-mazurskieWojewództwo podlaskieWojewództwo małopolskieWojewództwo śląskieWojewództwo opolskieWojewództwo dolnośląskieWojewództwo lubuskieWojewództwo podkarpackieWojewództwo świętokrzyskieWojewództwo łódzkieWojewództwo wielkopolskieWojewództwo zachodniopomorskieWojewództwo lubelskieWojewództwo mazowieckieWojewództwo kujawsko-pomorskieWojewództwo pomorskiePolandMoldova, Republic ofHungaryCzech RepublicBulgariaAustriaSwitzerlandGermanyBrandenburgThüringenSachsenSchleswig-HolsteinSachsen-AnhaltHamburgMecklenburg-VorpommernBayernHessenNiedersachsenBaden-WürttembergRheinland-PfalzNordrhein-WestfalenSaarlandBremenBerlinDenmarkNorwayVestlandTrøndelagRogalandAgderTelemarkBuskerudInnlandetAkershusOsloØstfoldVestfoldMøre og RomsdalSvalbard og Jan MayenNordlandTromsFinnmarkNorwaySwedenVarsinais-SuomiAhvenanmaaUusimaaSatakuntaKanta-HämePirkanmaaKeski-SuomiKymenlaaksoPäijät-HämePohjanmaaKeski-PohjanmaaEtelä-PohjanmaaEtelä-SavoPohjois-KarjalaKainuuEtelä-KarjalaPohjois-SavoPohjois-PohjanmaaLappiFinlandEstoniaLatviaLithuaniaBelarusNetherlandsZuid-HollandDrentheGelderlandLimburgZeelandNoord-BrabantUtrechtNoord-HollandFlevolandFrieslandGroningenOverijsselLuxembourgBelgiumUnited KingdomScotlandWalesEnglandNorthern IrelandIrelandIcelandSpainSpainSpainSpainSpainCantabriaAsturiasGaliciaLa RiojaNavarraComunidad de MadridAragónExtremaduraCastilla-La ManchaAndalucíaRegión de MurciaComunidad ValencianaCataluñaCanariasIslas BalearesPaís VascoCastilla y LeónCeutaMelillaPortugalCyprusTurkeyUkraineZakarpattia OblastTernopil OblastChernivtsi OblastIvano-Frankivsk OblastLviv OblastVolyn OblastMykolaiv OblastZaporizhzhia OblastCherkasy OblastKyivKyiv OblastVinnytsia OblastKhmelnytskyi OblastRivne OblastZhytomyr OblastKirovohrad OblastDonetsk OblastDnipropetrovsk OblastKharkiv OblastPoltava OblastLuhansk OblastChernihiv OblastSumy OblastSevastopolAutonomous Republic of CrimeaOdesa OblastKherson OblastIsle of ManMonacoGibraltarGuernseyJerseyLiechtensteinMaltaFaroe IslandsSri LankaTaiwanViet NamMyanmarCambodiaLao People's Democratic RepublicThailandPhilippinesMalaysiaOmanUnited Arab EmiratesYemenQatarKuwaitSaudi ArabiaIsraelLebanonSyrian Arab RepublicJordanIraqRussian FederationKrasnodar KraiKabardino-Balkaria, Republic ofIngushetia, Republic ofChechnya, Republic ofUlyanovsk OblastNizhny Novgorod OblastRyazan OblastMoscow OblastKaliningrad OblastKarachay-Cherkessia, Republic ofNorth Ossetia-Alania, Republic ofDagestan, Republic ofAstrakhan OblastVolgograd OblastPenza OblastTatarstan, Republic ofMari El, Republic ofUdmurtia, Republic ofKostroma OblastVladimir OblastTver OblastPskov OblastSaint PetersburgVologda OblastBelgorod OblastTambov OblastAdygea, Republic ofOryol OblastBryansk OblastKaluga OblastStavropol KraiKalmykia, Republic ofRostov OblastSaratov OblastSamara OblastMordovia, Republic ofChuvashia, Republic ofKirov OblastOrenburg OblastBashkortostan, Republic ofPerm KraiKomi, Republic ofIvanovo OblastYaroslavl OblastNovgorod OblastLeningrad OblastKarelia, Republic ofKurgan OblastChelyabinsk OblastSverdlovsk OblastKhabarovsk KraiSakha, Republic ofAmur OblastPrimorsky KraiMagadan OblastJewish Autonomous OblastChukotka Autonomous OkrugKamchatka KraiSakhalin OblastKrasnoyarsk KraiBuryatia, Republic ofZabaykalsky KraiKhakassia, Republic ofTuva, Republic ofIrkutsk OblastKemerovo OblastAltai, Republic ofAltai KraiNovosibirsk OblastOmsk OblastTomsk OblastTyumen OblastKhanty-Mansi Autonomous OkrugYamalo-Nenets Autonomous OkrugArkhangelsk OblastNenets Autonomous OkrugMurmansk OblastVoronezh OblastKursk OblastLipetsk OblastTula OblastSmolensk OblastMoscowAzerbaijanArmeniaGeorgiaKorea, Democratic People's Republic ofKorea, Republic ofJejudoIncheonDaeguBusanUlsanGyeongsangbuk-doGwangjuJeonnamGyeongsangnam-doJeonbukDaejeonSejongChungcheongbuk-doChungcheongnam-doSeoulGyeonggiGangwonJapanBangladeshBhutanNepalMongoliaAfghanistanPakistanKyrgyzstanIran, Islamic Republic ofTurkmenistanTajikistanUzbekistanIndiaKazakhstanChinaHong KongSingaporeMaldivesBahrainPalestine, State ofKiribatiNew ZealandBrunei DarussalamTimor-LesteIndonesiaPapua New GuineaAustralian Capital TerritoryTasmaniaNorthern TerritoryWestern AustraliaQueenslandNew South WalesVictoriaSouth AustraliaAustraliaTokelauNorfolk IslandGuamPitcairnNauruTuvaluMarshall IslandsAmerican SamoaCook IslandsNiueTongaPalauNorthern Mariana IslandsMicronesia, Federated States ofSamoaVanuatuFijiSolomon Islands \ No newline at end of file + + + + + + World Map + + + + + + Cuba + + + + + + + + + + + + + + Bonaire + + + + + + Jamaica + + + + + + Puerto Rico + + + + + + + + + + Dominican Republic + + + + + + Haiti + + + + + + El Salvador + + + + + + Guatemala + + + + + + Honduras + + + + + + Nicaragua + + + + + + Panama + + + + + + Costa Rica + + + + + + Mexico + + + + + + Jalisco + + + + + Aguascalientes + + + + + San Luis Potosí + + + + + Nuevo León + + + + + Tamaulipas + + + + + Coahuila + + + + + Quintana Roo + + + + + Yucatán + + + + + Guerrero + + + + + Campeche + + + + + Chiapas + + + + + Oaxaca + + + + + Ciudad de México + + + + + Morelos + + + + + Puebla + + + + + Hidalgo + + + + + México + + + + + Queretaro + + + + + Michoacan + + + + + Guanajuato + + + + + Tabasco + + + + + Veracruz + + + + + Colima + + + + + Zacatecas + + + + + Durango + + + + + Chihuahua + + + + + Sinaloa + + + + + Baja California + + + + + Sonora + + + + + Baja California Sur + + + + + Nayarit + + + + + + + Montserrat + + + + + + British Virgin Islands + + + + + + + + + US Virgin Islands + + + + + + + + + Saint Kitts and Nevis + + + + + + + + + Cayman Islands + + + + + + Anguilla + + + + + + Grenada + + + + + + Saint Lucia + + + + + + Saint Vincent and the Grenadines + + + + + + Turks and Caicos Islands + + + + + + + + + + Barbados + + + + + + Antigua and Barbuda + + + + + + + + + Sint Maarten (Dutch Part) + + + + + + Dominica + + + + + + Trinidad and Tobago + + + + + + + + + Bahamas + + + + + + + + + + + + + + + + + + + + + + + + + Belize + + + + + + + + + + + Washington + + + + + Delaware + + + + + Maryland + + + + + West Virginia + + + + + New York + + + + + New Jersey + + + + + Pennsylvania + + + + + Virginia + + + + + Kentucky + + + + + Ohio + + + + + Indiana + + + + + Illinois + + + + + Michigan + + + + + Wisconsin + + + + + Connecticut + + + + + Rhode Island + + + + + Vermont + + + + + New Hampshire + + + + + Massachusetts + + + + + Maine + + + + + Alabama + + + + + Georgia + + + + + South Carolina + + + + + Florida + + + + + Mississippi + + + + + Tennessee + + + + + North Carolina + + + + + Texas + + + + + Oklahoma + + + + + New Mexico + + + + + Nebraska + + + + + South Dakota + + + + + Kansas + + + + + Colorado + + + + + North Dakota + + + + + Arkansas + + + + + Missouri + + + + + Louisiana + + + + + Iowa + + + + + Minnesota + + + + + Arizona + + + + + Nevada + + + + + California + + + + + Utah + + + + + Oregon + + + + + Montana + + + + + Idaho + + + + + Wyoming + + + + + Hawaii + + + + + Alaska + + + + + Washington, District of Columbia + + + + + United States + + + + + + + + Manitoba + + + + + Northwest Territories + + + + + Newfoundland and Labrador + + + + + Nunavut + + + + + Quebec + + + + + British Columbia + + + + + Saskatchewan + + + + + Alberta + + + + + Ontario + + + + + New Brunswick + + + + + Nova Scotia + + + + + Prince Edward Island + + + + + Yukon + + + + + Canada + + + + + Bermuda + + + + + + + + Brazil + + + + + + São Paulo + + + + + Rio de Janeiro + + + + + Espírito Santo + + + + + Rio Grande do Sul + + + + + Santa Catarina + + + + + Paraná + + + + + Minas Gerais + + + + + Sergipe + + + + + Alagoas + + + + + Pernambuco + + + + + Paraíba + + + + + Rio Grande do Norte + + + + + Bahia + + + + + Ceará + + + + + Piauí + + + + + Maranhão + + + + + Mato Grosso do Sul + + + + + Mato Grosso + + + + + Pará + + + + + Amapá + + + + + Tocantins + + + + + Acre + + + + + Rondônia + + + + + Amazonas + + + + + Roraima + + + + + Goiás + + + + + Distrito Federal + + + + + + + + + + + Arica y Parinacota + + + + + Antofagasta + + + + + Tarapacá + + + + + Coquimbo + + + + + O'Higgins + + + + + Biobío + + + + + Los Ríos + + + + + Atacama + + + + + Región Metropolitana de Santiago + + + + + Valparaíso + + + + + Maule + + + + + Ñuble + + + + + Araucanía + + + + + Aysén + + + + + Los Lagos + + + + + Magallanes + + + + + Chile + + + + + + + Argentina + + + + + Provincia de Santa Cruz + + + + + Provincia del Chubut + + + + + Ciudad Autónoma de Buenos Aires + + + + + Provincia de Buenos Aires + + + + + Provincia de Neuquén + + + + + Provincia de Río Negro + + + + + Provincia de La Pampa + + + + + Provincia de Mendoza + + + + + Provincia de San Luis + + + + + Provincia de Córdoba + + + + + Provincia de Santa Fe + + + + + Provincia del Chaco + + + + + Provincia de Formosa + + + + + Provincia de La Rioja + + + + + Provincia de San Juan + + + + + Provincia de Catamarca + + + + + Provincia de Santiago del Estero + + + + + Provincia de Tucumán + + + + + Provincia de Salta + + + + + Provincia de Jujuy + + + + + Provincia de Entre Ríos + + + + + Provincia de Corrientes + + + + + Provincia de Misiones + + + + + Provincia de Tierra del Fuego + + + + + + + Suriname + + + + + + Guyana + + + + + + Venezuela, Bolivarian Republic of + + + + + + + + + Uruguay + + + + + + Paraguay + + + + + + Ecuador + + + + + + Colombia + + + + + + Departamento del Amazonas + + + + + Departamento del Putumayo + + + + + Departamento de Nariño + + + + + Departamento del Caquetá + + + + + Departamento de Guainía + + + + + Departamento del Vaupés + + + + + Departamento del Guaviare + + + + + Departamento del Meta + + + + + Departamento del Vichada + + + + + Departamento de Casanare + + + + + Departamento de Arauca + + + + + Departamento del Cauca + + + + + Departamento del Huila + + + + + Bogotá + + + + + Departamento del Tolima + + + + + Departamento de Santander + + + + + Departamento de Córdoba + + + + + Departamento de Sucre + + + + + Departamento del Valle del Cauca + + + + + Departamento del Quindío + + + + + Departamento del Risaralda + + + + + Departamento de Caldas + + + + + Departamento de Antioquia + + + + + Departamento del Chocó + + + + + Departamento de Bolívar + + + + + Departamento del Magdalena + + + + + Departamento del Atlántico + + + + + Departamento de Cundinamarca + + + + + Departamento de Boyacá + + + + + Norte de Santander + + + + + Departamento del Cesar + + + + + Departamento de La Guajira + + + + + San Andrés y Providencia + + + + + + + Peru + + + + + + Madre de Dios + + + + + Ucayali + + + + + Tacna + + + + + Moquegua + + + + + Apurímac + + + + + Ayacucho + + + + + Arequipa + + + + + Ica + + + + + Huancavelica + + + + + Puno + + + + + Cuzco + + + + + Cajamarca + + + + + San Martín + + + + + Piura + + + + + Tumbes + + + + + Lambayeque + + + + + Junín + + + + + Pasco + + + + + Huánuco + + + + + Áncash + + + + + Department of Lima + + + + + La Libertad + + + + + Amazonas + + + + + Loreto + + + + + + + Bolivia, Plurinational State of + + + + + + + + + Aruba + + + + + + South Georgia and the South Sandwich Islands + + + + + + Falkland Islands (Malvinas) + + + + + + + + + + + + + + + Curacao + + + + + + + + + Chad + + + + + + Algeria + + + + + + Egypt + + + + + + Libya + + + + + + Morocco + + + + + + Western Sahara + + + + + + Sudan + + + + + Tunisia + + + + + + + Niger + + + + + + Mauritania + + + + + + Mali + + + + + + Burkina Faso + + + + + Eritrea + + + + + + + Senegal + + + + + + Gambia + + + + + Guinea-Bissau + + + + + + Guinea + + + + + + Sierra Leone + + + + + + + Liberia + + + + + + Cote d'Ivoire + + + + + + Ghana + + + + + + Togo + + + + + + Benin + + + + + + Cameroon + + + + + + Central African Republic + + + + + + South Sudan + + + + + + Ethiopia + + + + + + Djibouti + + + + + Somalia + + + + + + + Equatorial Guinea + + + + + Gabon + + + + + + + Kenya + + + + + + Tanzania, United Republic of + + + + + + Uganda + + + + + + Burundi + + + + + + Rwanda + + + + + + Congo, the Democratic Republic of the + + + + + + Congo + + + + + + + + + Angola + + + + + + Zambia + + + + + + Mozambique + + + + + + Malawi + + + + + + Zimbabwe + + + + + + Namibia + + + + + + Botswana + + + + + Swaziland + + + + + + Lesotho + + + + + + Madagascar + + + + + + + Nigeria + + + + + South Africa + + + + + + Saint Helena, Ascension and Tristan Da Cunha + + + + + + Seychelles + + + + + + Cape Verde + + + + + + + + + + + + + + + Sao Tome and Principe + + + + + + + + + Mauritius + + + + + + Comoros + + + + + + + + + + + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + France + + + + + + + Polynésie Française + + + + + Wallis-et-Futuna + + + + + Nouvelle-Calédonie + + + + + Saint-Martin + + + + + Saint-Barthélemy + + + + + Saint-Pierre-et-Miquelon + + + + + Corse + + + + + Nouvelle-Aquitaine + + + + + Auvergne-Rhône-Alpes + + + + + Centre-Val de Loire + + + + + Bourgogne-Franche-Comté + + + + + Bretagne + + + + + Pays de la Loire + + + + + Grand Est + + + + + Normandie + + + + + Île-de-France + + + + + Hauts-de-France + + + + + Provence-Alpes-Côte d'Azur + + + + + Occitanie + + + + + La Réunion + + + + + Mayotte + + + + + Martinique + + + + + Guadeloupe + + + + + Terres Australes et Antarctiques Françaises + + + + + Guyane + + + + + France + + + + + + Slovenia + + + + + Serbia + + + + + + + Kosovo + + + + + + Montenegro + + + + + + Macedonia, the Former Yugoslav Republic of + + + + + Greece + + + + + + + + + + + + + Croatia + + + + + + Bosnia and Herzegovina + + + + + + Albania + + + + + Holy See (Vatican City State) + + + + + + San Marino + + + + + + + + + Puglia + + + + + Molise + + + + + Abruzzo + + + + + Marche + + + + + Emilia-Romagna + + + + + Lombardia + + + + + Lazio + + + + + Umbria + + + + + Veneto + + + + + Trentino-Alto Adige + + + + + Basilicata + + + + + Campania + + + + + Toscana + + + + + Liguria + + + + + Piemonte + + + + + Calabria + + + + + Friuli-Venezia Giulia + + + + + Valle d'Aosta + + + + + Sicilia + + + + + Sardegna + + + + + Italy + + + + + + Slovakia + + + + + + Romania + + + + + + + + Województwo warmińsko-mazurskie + + + + + Województwo podlaskie + + + + + Województwo małopolskie + + + + + Województwo śląskie + + + + + Województwo opolskie + + + + + Województwo dolnośląskie + + + + + Województwo lubuskie + + + + + Województwo podkarpackie + + + + + Województwo świętokrzyskie + + + + + Województwo łódzkie + + + + + Województwo wielkopolskie + + + + + Województwo zachodniopomorskie + + + + + Województwo lubelskie + + + + + Województwo mazowieckie + + + + + Województwo kujawsko-pomorskie + + + + + Województwo pomorskie + + + + + Poland + + + + + Moldova, Republic of + + + + + + + Hungary + + + + + + Czech Republic + + + + + + Bulgaria + + + + + + Austria + + + + + + Switzerland + + + + + + Germany + + + + + Brandenburg + + + + + Thüringen + + + + + Sachsen + + + + + Schleswig-Holstein + + + + + Sachsen-Anhalt + + + + + Hamburg + + + + + Mecklenburg-Vorpommern + + + + + Bayern + + + + + Hessen + + + + + Niedersachsen + + + + + Baden-Württemberg + + + + + Rheinland-Pfalz + + + + + Nordrhein-Westfalen + + + + + Saarland + + + + + Bremen + + + + + Berlin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Denmark + + + + + + + Norway + + + + + + Vestland + + + + + Trøndelag + + + + + Rogaland + + + + + Agder + + + + + Telemark + + + + + Buskerud + + + + + Innlandet + + + + + Akershus + + + + + Oslo + + + + + Østfold + + + + + Vestfold + + + + + Møre og Romsdal + + + + + Svalbard og Jan Mayen + + + + + Nordland + + + + + Troms + + + + + Finnmark + + + + + Norway + + + + + + Sweden + + + + + + + + Varsinais-Suomi + + + + + Ahvenanmaa + + + + + Uusimaa + + + + + Satakunta + + + + + Kanta-Häme + + + + + Pirkanmaa + + + + + Keski-Suomi + + + + + Kymenlaakso + + + + + Päijät-Häme + + + + + Pohjanmaa + + + + + Keski-Pohjanmaa + + + + + Etelä-Pohjanmaa + + + + + Etelä-Savo + + + + + Pohjois-Karjala + + + + + Kainuu + + + + + Etelä-Karjala + + + + + Pohjois-Savo + + + + + Pohjois-Pohjanmaa + + + + + Lappi + + + + + Finland + + + + + Estonia + + + + + + + Latvia + + + + + + Lithuania + + + + + + Belarus + + + + + Netherlands + + + + + + Zuid-Holland + + + + + Drenthe + + + + + Gelderland + + + + + Limburg + + + + + Zeeland + + + + + Noord-Brabant + + + + + Utrecht + + + + + Noord-Holland + + + + + Flevoland + + + + + Friesland + + + + + Groningen + + + + + Overijssel + + + + + + + + Luxembourg + + + + + + Belgium + + + + + + United Kingdom + + + + + Scotland + + + + + Wales + + + + + England + + + + + Northern Ireland + + + + + + + Ireland + + + + + + + Iceland + + + + + + Spain + + + + + + Spain + + + + + Spain + + + + + Spain + + + + + Spain + + + + + + + Cantabria + + + + + Asturias + + + + + Galicia + + + + + La Rioja + + + + + Navarra + + + + + Comunidad de Madrid + + + + + Aragón + + + + + Extremadura + + + + + Castilla-La Mancha + + + + + Andalucía + + + + + Región de Murcia + + + + + Comunidad Valenciana + + + + + Cataluña + + + + + Canarias + + + + + Islas Baleares + + + + + País Vasco + + + + + Castilla y León + + + + + Ceuta + + + + + Melilla + + + + + + + + + + + + + + + + + Portugal + + + + + Cyprus + + + + + + Turkey + + + + + + + + + Ukraine + + + + + + Zakarpattia Oblast + + + + + Ternopil Oblast + + + + + Chernivtsi Oblast + + + + + Ivano-Frankivsk Oblast + + + + + Lviv Oblast + + + + + Volyn Oblast + + + + + Mykolaiv Oblast + + + + + Zaporizhzhia Oblast + + + + + Cherkasy Oblast + + + + + Kyiv + + + + + Kyiv Oblast + + + + + Vinnytsia Oblast + + + + + Khmelnytskyi Oblast + + + + + Rivne Oblast + + + + + Zhytomyr Oblast + + + + + Kirovohrad Oblast + + + + + Donetsk Oblast + + + + + Dnipropetrovsk Oblast + + + + + Kharkiv Oblast + + + + + Poltava Oblast + + + + + Luhansk Oblast + + + + + Chernihiv Oblast + + + + + Sumy Oblast + + + + + Sevastopol + + + + + Autonomous Republic of Crimea + + + + + Odesa Oblast + + + + + Kherson Oblast + + + + + + + Isle of Man + + + + + + Monaco + + + + + + Gibraltar + + + + + + Guernsey + + + + + + Jersey + + + + + + Liechtenstein + + + + + + Malta + + + + + + Faroe Islands + + + + + + + + + + + + + + + Sri Lanka + + + + + + + + + Taiwan + + + + + + Viet Nam + + + + + + Myanmar + + + + + + Cambodia + + + + + Lao People's Democratic Republic + + + + + + + Thailand + + + + + Philippines + + + + + + Malaysia + + + + + + + + + Oman + + + + + + + + + United Arab Emirates + + + + + + Yemen + + + + + + + Qatar + + + + + + Kuwait + + + + + + Saudi Arabia + + + + + Israel + + + + + + + Lebanon + + + + + Syrian Arab Republic + + + + + + Jordan + + + + + + Iraq + + + + + + Russian Federation + + + + + + Krasnodar Krai + + + + + Kabardino-Balkaria, Republic of + + + + + Ingushetia, Republic of + + + + + Chechnya, Republic of + + + + + Ulyanovsk Oblast + + + + + Nizhny Novgorod Oblast + + + + + Ryazan Oblast + + + + + Moscow Oblast + + + + + Kaliningrad Oblast + + + + + Karachay-Cherkessia, Republic of + + + + + North Ossetia-Alania, Republic of + + + + + Dagestan, Republic of + + + + + Astrakhan Oblast + + + + + Volgograd Oblast + + + + + Penza Oblast + + + + + Tatarstan, Republic of + + + + + Mari El, Republic of + + + + + Udmurtia, Republic of + + + + + Kostroma Oblast + + + + + Vladimir Oblast + + + + + Tver Oblast + + + + + Pskov Oblast + + + + + Saint Petersburg + + + + + Vologda Oblast + + + + + Belgorod Oblast + + + + + Tambov Oblast + + + + + Adygea, Republic of + + + + + Oryol Oblast + + + + + Bryansk Oblast + + + + + Kaluga Oblast + + + + + Stavropol Krai + + + + + Kalmykia, Republic of + + + + + Rostov Oblast + + + + + Saratov Oblast + + + + + Samara Oblast + + + + + Mordovia, Republic of + + + + + Chuvashia, Republic of + + + + + Kirov Oblast + + + + + Orenburg Oblast + + + + + Bashkortostan, Republic of + + + + + Perm Krai + + + + + Komi, Republic of + + + + + Ivanovo Oblast + + + + + Yaroslavl Oblast + + + + + Novgorod Oblast + + + + + Leningrad Oblast + + + + + Karelia, Republic of + + + + + Kurgan Oblast + + + + + Chelyabinsk Oblast + + + + + Sverdlovsk Oblast + + + + + Khabarovsk Krai + + + + + Sakha, Republic of + + + + + Amur Oblast + + + + + Primorsky Krai + + + + + Magadan Oblast + + + + + Jewish Autonomous Oblast + + + + + Chukotka Autonomous Okrug + + + + + Kamchatka Krai + + + + + Sakhalin Oblast + + + + + Krasnoyarsk Krai + + + + + Buryatia, Republic of + + + + + Zabaykalsky Krai + + + + + Khakassia, Republic of + + + + + Tuva, Republic of + + + + + Irkutsk Oblast + + + + + Kemerovo Oblast + + + + + Altai, Republic of + + + + + Altai Krai + + + + + Novosibirsk Oblast + + + + + Omsk Oblast + + + + + Tomsk Oblast + + + + + Tyumen Oblast + + + + + Khanty-Mansi Autonomous Okrug + + + + + Yamalo-Nenets Autonomous Okrug + + + + + Arkhangelsk Oblast + + + + + Nenets Autonomous Okrug + + + + + Murmansk Oblast + + + + + Voronezh Oblast + + + + + Kursk Oblast + + + + + Lipetsk Oblast + + + + + Tula Oblast + + + + + Smolensk Oblast + + + + + Moscow + + + + + + + Azerbaijan + + + + + + + + + Armenia + + + + + + Georgia + + + + + + Korea, Democratic People's Republic of + + + + + + Korea, Republic of + + + + + + Jejudo + + + + + Incheon + + + + + Daegu + + + + + Busan + + + + + Ulsan + + + + + Gyeongsangbuk-do + + + + + Gwangju + + + + + Jeonnam + + + + + Gyeongsangnam-do + + + + + Jeonbuk + + + + + Daejeon + + + + + Sejong + + + + + Chungcheongbuk-do + + + + + Chungcheongnam-do + + + + + Seoul + + + + + Gyeonggi + + + + + Gangwon + + + + + + + Japan + + + + + + + + + + + + + Bangladesh + + + + + + Bhutan + + + + + + Nepal + + + + + + Mongolia + + + + + + Afghanistan + + + + + + Pakistan + + + + + + Kyrgyzstan + + + + + + Iran, Islamic Republic of + + + + + + Turkmenistan + + + + + + Tajikistan + + + + + + Uzbekistan + + + + + + India + + + + + + Kazakhstan + + + + + + + + + China + + + + + + + + Hong Kong + + + + + + + + + + Singapore + + + + + + Maldives + + + + + + Bahrain + + + + + + Palestine, State of + + + + + + + + + + + + + + + + + Kiribati + + + + + + + + + New Zealand + + + + + + + + + + + + + Brunei Darussalam + + + + + + + + + Timor-Leste + + + + + Indonesia + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Papua New Guinea + + + + + + + + Australian Capital Territory + + + + + Tasmania + + + + + Northern Territory + + + + + Western Australia + + + + + Queensland + + + + + New South Wales + + + + + Victoria + + + + + South Australia + + + + + Australia + + + + + Tokelau + + + + + + Norfolk Island + + + + + + Guam + + + + + + Pitcairn + + + + + + Nauru + + + + + + Tuvalu + + + + + + Marshall Islands + + + + + + American Samoa + + + + + + Cook Islands + + + + + + + + + Niue + + + + + + Tonga + + + + + + + + + Palau + + + + + + Northern Mariana Islands + + + + + + Micronesia, Federated States of + + + + + + Samoa + + + + + + + + + Vanuatu + + + + + + + + + + + + + + + + + + + + + Fiji + + + + + + + + + + + + + + + Solomon Islands + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pointercrate-example/static/ftl/en-us/footer.ftl b/pointercrate-example/static/ftl/en-us/footer.ftl index 38082582..e8479625 100644 --- a/pointercrate-example/static/ftl/en-us/footer.ftl +++ b/pointercrate-example/static/ftl/en-us/footer.ftl @@ -3,5 +3,10 @@ footer-demonlist = Demonlist .extended-list = { extended-list } .legacy-list = { legacy-list } +footer-ratedplus = Rated+ List + .top-demon = Current Top Rated+ Demon + .extended-list = { extended-list } + .legacy-list = { legacy-list } + footer-tweet = Tweet Us: .developer = Developer \ No newline at end of file diff --git a/pointercrate-example/static/ftl/en-us/nav.ftl b/pointercrate-example/static/ftl/en-us/nav.ftl index bca3d5e8..99c0826b 100644 --- a/pointercrate-example/static/ftl/en-us/nav.ftl +++ b/pointercrate-example/static/ftl/en-us/nav.ftl @@ -3,4 +3,9 @@ nav-demonlist = Demonlist .record-submitter = Record Submitter .time-machine = Time Machine +nav-ratedplus = Rated+ List + .stats-viewer = Stats Viewer + .record-submitter = Record Submitter + .time-machine = Time Machine + nav-userarea = User Area \ No newline at end of file From 95cf346a1b3f6c5e25fa607800d63eeb3d46fd75 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:44:17 -0400 Subject: [PATCH 10/19] updated css so rated+ pages' primary colors are different --- pointercrate-core-pages/static/css/main.css | 6 ++++++ pointercrate-core-pages/static/css/ui.css | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/pointercrate-core-pages/static/css/main.css b/pointercrate-core-pages/static/css/main.css index 858e2fce..c8c59652 100644 --- a/pointercrate-core-pages/static/css/main.css +++ b/pointercrate-core-pages/static/css/main.css @@ -47,6 +47,12 @@ --color-unobstrusive-button-text-click: #111; } +html[data-list="ratedplus"] { + --color-pc-button-bg: #b30205; + --color-pc-button-bg-hover: #850204; + --color-pc-button-bg-click: #520203; +} + body { color: var(--color-text); diff --git a/pointercrate-core-pages/static/css/ui.css b/pointercrate-core-pages/static/css/ui.css index acff6ee9..404975cb 100644 --- a/pointercrate-core-pages/static/css/ui.css +++ b/pointercrate-core-pages/static/css/ui.css @@ -479,6 +479,10 @@ p.error { height: 100vh; width: 100vw; } + + html[data-list="ratedplus"] #bg { + background-image: url(/static/images/squares4.png); + } } @media (min-width: 768px) { From bf54e6b0998060f14a3ab4cc46428ada427991d8 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:44:47 -0400 Subject: [PATCH 11/19] updated -example page config --- pointercrate-example/src/main.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/pointercrate-example/src/main.rs b/pointercrate-example/src/main.rs index 035c5240..cfe21e86 100644 --- a/pointercrate-example/src/main.rs +++ b/pointercrate-example/src/main.rs @@ -196,11 +196,24 @@ fn page_configuration() -> PageConfiguration { let nav_bar = NavigationBar::new("/static/images/path/to/your/logo.png") .with_item( TopLevelNavigationBarItem::new( - Some("/demonlist/"), - // Pointercrate uses the "maud" create as its templating engine. + Some("/ratedplus/"), + // Pointercrate uses the "maud" create as its templating engine. // It allows you to describe HTML via Rust macros that allow you to dynamically generate content using // a Rust-like syntax and by interpolating and Rust variables from surrounding scopes (as long as the // implement the `Render` trait). See https://maud.lambda.xyz/ for details. + html! { + span { + (tr("nav-ratedplus")) + } + }, + ) + .with_sub_item(Some("/ratedplus/statsviewer/"), html! { (tr("nav-ratedplus.stats-viewer")) }) + .with_sub_item(Some("/ratedplus/?submitter=true"), html! { (tr("nav-ratedplus.record-submitter")) }) + .with_sub_item(Some("/ratedplus/?timemachine=true"), html! { (tr("nav-ratedplus.time-machine")) }), + ) + .with_item( + TopLevelNavigationBarItem::new( + Some("/demonlist/"), html! { span { (tr("nav-demonlist")) @@ -245,6 +258,20 @@ fn page_configuration() -> PageConfiguration { ), ], }) + .with_column(FooterColumn::LinkList { + heading: tr("footer-ratedplus"), + links: vec![ + Link::new("/ratedplus/1/", tr("footer-ratedplus.top-demon")), + Link::new( + format!("/ratedplus/{}/", pointercrate_demonlist::config::list_size() + 1), + tr("footer-ratedplus.extended-list"), + ), + Link::new( + format!("/ratedplus/{}/", pointercrate_demonlist::config::extended_list_size() + 1), + tr("footer-ratedplus.legacy-list"), + ), + ], + }) // Some links to social media, for example your twitter .with_link("https://twitter.com/stadust1971", tr("footer-tweet.developer")); From f2c587af6c7f4ec236874f1042522fbf6e77e919 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Wed, 1 Oct 2025 16:45:08 -0400 Subject: [PATCH 12/19] other changes --- pointercrate-core-api/src/preferences.rs | 10 ++++--- .../src/nationality/paginate.rs | 28 ++++++++++++------- pointercrate-demonlist/src/record/post.rs | 5 ++-- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/pointercrate-core-api/src/preferences.rs b/pointercrate-core-api/src/preferences.rs index 5e230193..392b9172 100644 --- a/pointercrate-core-api/src/preferences.rs +++ b/pointercrate-core-api/src/preferences.rs @@ -4,7 +4,7 @@ use crate::localization::LOCALE_COOKIE_NAME; use pointercrate_core::error::CoreError; use pointercrate_core::localization::LocaleConfiguration; use rocket::{ - http::CookieJar, + http::{Cookie, CookieJar}, request::{FromRequest, Outcome}, Request, }; @@ -14,13 +14,15 @@ pub struct ClientPreferences<'k, 'v>(HashMap<&'k str, &'v str>); impl<'k: 'v, 'v> ClientPreferences<'k, 'v> { /// Retrieve a particular preference which was sent to us from the client. - /// - /// `T` must implement `From`, which [`String`] already - /// implements, in case the untouched cookie value is what needs to be handled. pub fn get(&self, name: &'k str) -> Option<&'v str> { self.0.get(name).copied() } + /// Set a preference to a particular value. + pub fn set(name: &str, value: &str, cookies: &'v CookieJar<'v>) { + cookies.add(Cookie::new(format!("preference-{}", name), value.to_string())); + } + pub fn from_cookies(cookies: &'v CookieJar<'v>, preference_manager: &'k PreferenceManager) -> Self { ClientPreferences( preference_manager diff --git a/pointercrate-demonlist/src/nationality/paginate.rs b/pointercrate-demonlist/src/nationality/paginate.rs index 9550c899..796161c8 100644 --- a/pointercrate-demonlist/src/nationality/paginate.rs +++ b/pointercrate-demonlist/src/nationality/paginate.rs @@ -1,14 +1,18 @@ use crate::{ error::Result, + list::List, nationality::{Continent, Nationality}, }; use futures::StreamExt; use pointercrate_core::util::non_nullable; use serde::{Deserialize, Serialize}; -use sqlx::PgConnection; +use sqlx::{PgConnection, Row}; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct NationalityRankingPagination { + #[serde(default, deserialize_with = "non_nullable")] + list: Option, + #[serde(default, deserialize_with = "non_nullable")] continent: Option, @@ -26,12 +30,16 @@ pub struct RankedNation { impl NationalityRankingPagination { pub async fn page(&self, connection: &mut PgConnection) -> Result> { - let mut stream = sqlx::query!( - r#"SELECT rank as "rank!", score as "score!", nation as "nation!", iso_country_code as "iso_country_code!" FROM ranked_nations WHERE (STRPOS(nation, $1::CITEXT) > - 0 OR $1 is NULL) AND (continent::text = $2 OR $2 IS NULL)"#, - self.name_contains, - self.continent.map(|c| c.to_sql()) + let mut stream = sqlx::query( + match self.list.unwrap_or_default() { + List::Demonlist => + r#"SELECT rank, score, nation, iso_country_code FROM ranked_nations WHERE rank IS NOT NULL AND (STRPOS(nation, $1::CITEXT) > 0 OR $1 is NULL) AND (continent::text = $2 OR $2 IS NULL)"#, + List::RatedPlus => + r#"SELECT unrated_rank as rank, unrated_score as score, nation, iso_country_code FROM ranked_nations WHERE unrated_rank IS NOT NULL AND (STRPOS(nation, $1::CITEXT) > 0 OR $1 is NULL) AND (continent::text = $2 OR $2 IS NULL)"# + } ) + .bind(&self.name_contains) + .bind(self.continent.map(|c| c.to_sql())) .fetch(connection); let mut nations = Vec::new(); @@ -40,11 +48,11 @@ impl NationalityRankingPagination { let row = row?; nations.push(RankedNation { - rank: row.rank, - score: row.score, + rank: row.get("rank"), + score: row.get("score"), nationality: Nationality { - iso_country_code: row.iso_country_code, - nation: row.nation, + iso_country_code: row.get("iso_country_code"), + nation: row.get("nation"), subdivision: None, }, }) diff --git a/pointercrate-demonlist/src/record/post.rs b/pointercrate-demonlist/src/record/post.rs index 5096ca85..a1e4808d 100644 --- a/pointercrate-demonlist/src/record/post.rs +++ b/pointercrate-demonlist/src/record/post.rs @@ -96,13 +96,13 @@ impl NormalizedSubmission { } // Cannot submit records for the legacy list (it is possible to directly add them for list mods) - if self.demon.position > crate::config::extended_list_size() && self.status == RecordStatus::Submitted { + if self.demon.is_all_legacy() && self.status == RecordStatus::Submitted { return Err(DemonlistError::SubmitLegacy); } // Can only submit 100% records for the extended list (it is possible to directly add them for list // mods) - if self.demon.position > crate::config::list_size() && self.progress != 100 && self.status == RecordStatus::Submitted { + if !self.demon.is_any_main() && self.progress != 100 && self.status == RecordStatus::Submitted { return Err(DemonlistError::Non100Extended); } @@ -240,6 +240,7 @@ mod tests { demon: MinimalDemon { id: 1, position: 1, + rated_position: Some(1), name: "Bloodbath".to_string(), }, status: RecordStatus::Submitted, From e6bda5b5342d8e72cc004b59dd17ed3ab186ad39 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Tue, 14 Oct 2025 19:59:01 -0400 Subject: [PATCH 13/19] russian translations --- pointercrate-demonlist-pages/static/ftl/ru-ru/demon.ftl | 8 ++++++++ pointercrate-demonlist-pages/static/ftl/ru-ru/list.ftl | 2 ++ .../static/ftl/ru-ru/overview.ftl | 1 + pointercrate-example/static/ftl/ru-ru/footer.ftl | 5 +++++ pointercrate-example/static/ftl/ru-ru/nav.ftl | 5 +++++ 5 files changed, 21 insertions(+) create mode 100644 pointercrate-demonlist-pages/static/ftl/ru-ru/list.ftl diff --git a/pointercrate-demonlist-pages/static/ftl/ru-ru/demon.ftl b/pointercrate-demonlist-pages/static/ftl/ru-ru/demon.ftl index 00db9e56..1d0e93a1 100644 --- a/pointercrate-demonlist-pages/static/ftl/ru-ru/demon.ftl +++ b/pointercrate-demonlist-pages/static/ftl/ru-ru/demon.ftl @@ -46,6 +46,10 @@ demon-publisher = Публикатор demon-verifier = Верифер .validator-valuemissing = Пожалуйста, укажите верифера +demon-rated = Оценен + .yes = Да (Оценен) + .no = Нет (Не оценен) + demon-creators = Создатели demon-headline-by = от { $creator } @@ -86,6 +90,10 @@ movements-reason = Причина .moved = Перемещён .movedabove = { $demon } был перемещён выше этого демона .movedbelow = { $demon } был перемещён ниже этого демона + .rated = Оценен + .unrated = Не оценен + .otherrated = { $demon } получил оценку + .otherunrated = { $demon } потерял оценку ## Records table demon-records = Рекорды diff --git a/pointercrate-demonlist-pages/static/ftl/ru-ru/list.ftl b/pointercrate-demonlist-pages/static/ftl/ru-ru/list.ftl new file mode 100644 index 00000000..deb6a33b --- /dev/null +++ b/pointercrate-demonlist-pages/static/ftl/ru-ru/list.ftl @@ -0,0 +1,2 @@ +list-demonlist = Демонлист +list-ratedplus = Rated+ \ No newline at end of file diff --git a/pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl b/pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl index d8e1176f..3c9bb0c1 100644 --- a/pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl +++ b/pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl @@ -23,6 +23,7 @@ time-machine = Машина времени .active-position = Сейчас на #{ $position } .active-position-legacy = Сейчас в Legacy-листе + .active-position-none = На данный момент находится не в этом листе .active-info = Вы сейчас смотрите на демонлист, каким он был .return = Вернуться к настоящему diff --git a/pointercrate-example/static/ftl/ru-ru/footer.ftl b/pointercrate-example/static/ftl/ru-ru/footer.ftl index a7c5baa3..3ded72b8 100644 --- a/pointercrate-example/static/ftl/ru-ru/footer.ftl +++ b/pointercrate-example/static/ftl/ru-ru/footer.ftl @@ -3,5 +3,10 @@ footer-demonlist = Демонлист .extended-list = { extended-list } .legacy-list = { legacy-list } +footer-ratedplus = Список Rated+ + .top-demon = Нынешний сложнейший Rated+ демон + .extended-list = { list-ratedplus } { extended-list } + .legacy-list = { list-ratedplus } { legacy-list } + footer-tweet = Твитните нам: .developer = Разработчик \ No newline at end of file diff --git a/pointercrate-example/static/ftl/ru-ru/nav.ftl b/pointercrate-example/static/ftl/ru-ru/nav.ftl index 15f107e4..0a38f29e 100644 --- a/pointercrate-example/static/ftl/ru-ru/nav.ftl +++ b/pointercrate-example/static/ftl/ru-ru/nav.ftl @@ -3,4 +3,9 @@ nav-demonlist = Демонлист .record-submitter = Форма отправки рекордов .time-machine = Машина времени +nav-ratedplus = Список Rated+ + .stats-viewer = Панель статистики + .record-submitter = Форма отправки рекордов + .time-machine = Машина времени + nav-userarea = Личный кабинет \ No newline at end of file From 02b0f3fae7b3283479ffbc8316881e2128ce4dad Mon Sep 17 00:00:00 2001 From: jasonyess Date: Tue, 14 Oct 2025 20:05:47 -0400 Subject: [PATCH 14/19] remove mention of rate status in main list description --- pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl | 2 +- pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl b/pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl index 03ac2e89..45a27a0e 100644 --- a/pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl +++ b/pointercrate-demonlist-pages/static/ftl/en-us/overview.ftl @@ -1,5 +1,5 @@ main-list = Main List - .info = The main section of the Demonlist. These demons are the hardest rated levels in the game. Records are accepted above a given threshold and award a large amount of points! + .info = The main section of the Demonlist. These demons are the hardest levels in the game. Records are accepted above a given threshold and award a large amount of points! extended-list = Extended List .info = These are demons that dont qualify for the main section of the list, but are still of high relevance. Only 100% records are accepted for these demons! Note that non-100% that were submitted/approved before a demon fell off the main list will be retained. diff --git a/pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl b/pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl index 3c9bb0c1..13593202 100644 --- a/pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl +++ b/pointercrate-demonlist-pages/static/ftl/ru-ru/overview.ftl @@ -1,5 +1,5 @@ main-list = Main-лист - .info = Основная часть демонлиста. Эти уровни являются сложнейшими оцененными демонами в игре. Рекорды принимаются с прогрессом выше определенного значения и дают большое количество очков! + .info = Основная часть демонлиста. Эти уровни являются сложнейшими демонами в игре. Рекорды принимаются с прогрессом выше определенного значения и дают большое количество очков! extended-list = Extended-лист .info = Эти демоны больше не подходят для основной части листа, но все еще имеют высокую актуальность. Для этих демонов принимаются только прохождения! Учтите, что прогрессы, отправленные либо принятые до выпадения демона из main-листа сохранятся в списке. From 13e4e73dd1a56f4a3adb9aa691aee5942bbe5b0e Mon Sep 17 00:00:00 2001 From: jasonyess Date: Tue, 14 Oct 2025 20:26:26 -0400 Subject: [PATCH 15/19] remove unnecessary trait impls --- pointercrate-demonlist-api/src/list.rs | 2 +- pointercrate-demonlist/src/list.rs | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/pointercrate-demonlist-api/src/list.rs b/pointercrate-demonlist-api/src/list.rs index 256982a3..4fe84332 100644 --- a/pointercrate-demonlist-api/src/list.rs +++ b/pointercrate-demonlist-api/src/list.rs @@ -27,7 +27,7 @@ impl<'a> FromUriParam for ClientList { type Target = ClientList; fn from_uri_param(param: &'a str) -> Self::Target { - ClientList(List::from(param)) + ClientList(List::from_str(param).unwrap_or_default()) } } diff --git a/pointercrate-demonlist/src/list.rs b/pointercrate-demonlist/src/list.rs index cc07ba72..0de0c64f 100644 --- a/pointercrate-demonlist/src/list.rs +++ b/pointercrate-demonlist/src/list.rs @@ -31,21 +31,6 @@ impl Default for List { } } -impl From<&str> for List { - fn from(value: &str) -> Self { - List::from_str(value).unwrap_or_default() - } -} - -impl From> for List { - fn from(value: Option<&str>) -> Self { - match value { - Some(list) => List::from(list), - None => List::default(), - } - } -} - impl FromStr for List { type Err = CoreError; From 097a0ebaf3e4c022cb52c2f74cc43054236e6632 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Tue, 14 Oct 2025 21:51:45 -0400 Subject: [PATCH 16/19] custom list serialization impl because i think i need one --- pointercrate-demonlist-api/src/list.rs | 5 +---- pointercrate-demonlist/src/list.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pointercrate-demonlist-api/src/list.rs b/pointercrate-demonlist-api/src/list.rs index 4fe84332..10de6f56 100644 --- a/pointercrate-demonlist-api/src/list.rs +++ b/pointercrate-demonlist-api/src/list.rs @@ -16,10 +16,7 @@ impl<'a> FromParam<'a> for ClientList { type Error = CoreError; fn from_param(param: &'a str) -> Result { - match List::from_str(param) { - Ok(list) => Ok(ClientList(list)), - Err(err) => Err(err), - } + Ok(ClientList(List::from_str(param)?)) } } diff --git a/pointercrate-demonlist/src/list.rs b/pointercrate-demonlist/src/list.rs index 0de0c64f..ae52e531 100644 --- a/pointercrate-demonlist/src/list.rs +++ b/pointercrate-demonlist/src/list.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use pointercrate_core::error::CoreError; use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Eq, Serialize, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum List { Demonlist, // only consists of rated demons (Demonlist) RatedPlus, // consists of ALL demons (Rated+ List) @@ -49,6 +49,15 @@ impl ToString for List { } } +impl Serialize for List { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + impl<'de> Deserialize<'de> for List { fn deserialize(deserializer: D) -> Result where From a57294d06cabf53242db955ab81a8f5d524dbf8a Mon Sep 17 00:00:00 2001 From: jasonyess Date: Fri, 24 Oct 2025 18:14:28 -0400 Subject: [PATCH 17/19] rename `unrated_x` fields to `ratedplus_x` --- .../20250722002114_unrated_levels.down.sql | 6 ++-- .../20250722002114_unrated_levels.up.sql | 36 +++++++++---------- pointercrate-demonlist-api/src/pages.rs | 8 ++--- .../sql/paginate_player_ranking.sql | 10 +++--- .../sql/paginate_players_by_id.sql | 2 +- pointercrate-demonlist/src/nationality/mod.rs | 4 +-- .../src/nationality/paginate.rs | 2 +- pointercrate-demonlist/src/player/get.rs | 6 ++-- pointercrate-demonlist/src/player/mod.rs | 8 ++--- pointercrate-demonlist/src/player/paginate.rs | 4 +-- 10 files changed, 43 insertions(+), 43 deletions(-) diff --git a/migrations/20250722002114_unrated_levels.down.sql b/migrations/20250722002114_unrated_levels.down.sql index fa66d2bf..861c1f47 100644 --- a/migrations/20250722002114_unrated_levels.down.sql +++ b/migrations/20250722002114_unrated_levels.down.sql @@ -190,9 +190,9 @@ ALTER TABLE demons DROP COLUMN rated_position; DROP FUNCTION recompute_rated_positions(); ALTER TABLE demons DROP COLUMN rated; -ALTER TABLE players DROP COLUMN unrated_score; -ALTER TABLE nationalities DROP COLUMN unrated_score; -ALTER TABLE subdivisions DROP COLUMN unrated_score; +ALTER TABLE players DROP COLUMN ratedplus_score; +ALTER TABLE nationalities DROP COLUMN ratedplus_score; +ALTER TABLE subdivisions DROP COLUMN ratedplus_score; ALTER TABLE demon_modifications DROP COLUMN rated; ALTER TABLE demon_modifications DROP COLUMN rated_position; \ No newline at end of file diff --git a/migrations/20250722002114_unrated_levels.up.sql b/migrations/20250722002114_unrated_levels.up.sql index b8758643..d472c911 100644 --- a/migrations/20250722002114_unrated_levels.up.sql +++ b/migrations/20250722002114_unrated_levels.up.sql @@ -1,7 +1,7 @@ ALTER TABLE demons ADD COLUMN rated BOOLEAN NOT NULL DEFAULT TRUE; -ALTER TABLE players ADD COLUMN unrated_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; -ALTER TABLE nationalities ADD COLUMN unrated_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; -ALTER TABLE subdivisions ADD COLUMN unrated_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; +ALTER TABLE players ADD COLUMN ratedplus_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; +ALTER TABLE nationalities ADD COLUMN ratedplus_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; +ALTER TABLE subdivisions ADD COLUMN ratedplus_score DOUBLE PRECISION NOT NULL DEFAULT 0.0; ALTER TABLE demons ADD COLUMN rated_position SMALLINT DEFAULT NULL; UPDATE demons SET rated_position = position; @@ -65,11 +65,11 @@ $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION recompute_player_scores() RETURNS void AS $$ UPDATE players - SET score = COALESCE(q.score, 0), unrated_score = COALESCE(q.unrated_score, 0) + SET score = COALESCE(q.score, 0), ratedplus_score = COALESCE(q.ratedplus_score, 0) FROM ( SELECT player, SUM(record_score(progress, position, 150, requirement)) - FILTER (WHERE NOT rated_list) AS unrated_score, + FILTER (WHERE NOT rated_list) AS ratedplus_score, SUM(record_score(progress, position, 150, requirement)) FILTER (WHERE rated_list) AS score FROM score_giving @@ -92,13 +92,13 @@ $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION recompute_nation_scores() RETURNS void AS $$ UPDATE nationalities - SET score = COALESCE(p.score, 0), unrated_score = COALESCE(p.unrated_score, 0) + SET score = COALESCE(p.score, 0), ratedplus_score = COALESCE(p.ratedplus_score, 0) FROM ( SELECT nationality, SUM(record_score(q.progress, q.position, 150, q.requirement)) FILTER (WHERE q.rated_list) AS score, SUM(record_score(q.progress, q.position, 150, q.requirement)) - FILTER (WHERE NOT q.rated_list) AS unrated_score + FILTER (WHERE NOT q.rated_list) AS ratedplus_score FROM ( SELECT DISTINCT ON (position, nationality, rated_list) * from score_giving INNER JOIN players @@ -127,13 +127,13 @@ $$ LANGUAGE SQL; CREATE OR REPLACE FUNCTION recompute_subdivision_scores() RETURNS void AS $$ UPDATE subdivisions - SET score = COALESCE(p.score, 0), unrated_score = COALESCE(p.unrated_score, 0) + SET score = COALESCE(p.score, 0), ratedplus_score = COALESCE(p.ratedplus_score, 0) FROM ( SELECT nationality, subdivision, SUM(record_score(q.progress, q.position, 150, q.requirement)) FILTER (WHERE q.rated_list) AS score, SUM(record_score(q.progress, q.position, 150, q.requirement)) - FILTER (WHERE NOT q.rated_list) AS unrated_score + FILTER (WHERE NOT q.rated_list) AS ratedplus_score FROM ( SELECT DISTINCT ON (position, nationality, subdivision, rated_list) * from score_giving INNER JOIN players @@ -158,20 +158,20 @@ DROP MATERIALIZED VIEW player_ranks; CREATE MATERIALIZED VIEW player_ranks AS SELECT CASE WHEN score != 0 THEN RANK() OVER (ORDER BY score DESC) END AS rank, - CASE WHEN unrated_score != 0 THEN RANK() OVER (ORDER BY unrated_score DESC) END AS unrated_rank, + CASE WHEN ratedplus_score != 0 THEN RANK() OVER (ORDER BY ratedplus_score DESC) END AS ratedplus_rank, id FROM players -WHERE unrated_score != 0 OR score != 0 AND NOT banned; +WHERE ratedplus_score != 0 OR score != 0 AND NOT banned; CREATE UNIQUE INDEX player_ranks_id_idx ON player_ranks(id); CREATE VIEW ranked_players AS SELECT ROW_NUMBER() OVER(ORDER BY rank, id) AS index, - ROW_NUMBER() OVER (ORDER BY unrated_rank, id) AS unrated_index, + ROW_NUMBER() OVER (ORDER BY ratedplus_rank, id) AS ratedplus_index, rank, - unrated_rank, - id, name, players.score, players.unrated_score, + ratedplus_rank, + id, name, players.score, players.ratedplus_score, subdivision, nationalities.iso_country_code, nationalities.nation, @@ -186,16 +186,16 @@ DROP VIEW ranked_nations; CREATE VIEW ranked_nations AS SELECT ROW_NUMBER() OVER (ORDER BY score DESC, iso_country_code) AS index, - ROW_NUMBER() OVER (ORDER BY unrated_score DESC, iso_country_code) AS unrated_index, + ROW_NUMBER() OVER (ORDER BY ratedplus_score DESC, iso_country_code) AS ratedplus_index, CASE WHEN score != 0 THEN RANK() OVER (ORDER BY score DESC) END AS rank, - CASE WHEN unrated_score != 0 THEN RANK() OVER (ORDER BY unrated_score DESC) END AS unrated_rank, + CASE WHEN ratedplus_score != 0 THEN RANK() OVER (ORDER BY ratedplus_score DESC) END AS ratedplus_rank, score, - unrated_score, + ratedplus_score, iso_country_code, nation, continent FROM nationalities - WHERE score > 0.0 OR unrated_score > 0.0; + WHERE score > 0.0 OR ratedplus_score > 0.0; -- audit log stuff ALTER TABLE demon_modifications ADD COLUMN rated BOOLEAN NULL DEFAULT NULL; diff --git a/pointercrate-demonlist-api/src/pages.rs b/pointercrate-demonlist-api/src/pages.rs index de65b1d6..26603a5b 100644 --- a/pointercrate-demonlist-api/src/pages.rs +++ b/pointercrate-demonlist-api/src/pages.rs @@ -213,9 +213,9 @@ pub async fn heatmap_css(list: ClientList, pool: &State) -> Re let mut nation_scores = HashMap::new(); let mut nations_stream = sqlx::query!( - r#"SELECT iso_country_code, CASE WHEN $1 THEN score ELSE unrated_score END as score + r#"SELECT iso_country_code, CASE WHEN $1 THEN score ELSE ratedplus_score END as score FROM nationalities - WHERE CASE WHEN $1 THEN score ELSE unrated_score END > 0.0"#, + WHERE CASE WHEN $1 THEN score ELSE ratedplus_score END > 0.0"#, list.0 == List::Demonlist ) .fetch(&mut *connection); @@ -239,9 +239,9 @@ pub async fn heatmap_css(list: ClientList, pool: &State) -> Re drop(nations_stream); let mut subdivisions_stream = sqlx::query!( - r#"SELECT nation, iso_code, CASE WHEN $1 THEN score ELSE unrated_score END as score + r#"SELECT nation, iso_code, CASE WHEN $1 THEN score ELSE ratedplus_score END as score FROM subdivisions - WHERE CASE WHEN $1 THEN score ELSE unrated_score END > 0.0"#, + WHERE CASE WHEN $1 THEN score ELSE ratedplus_score END > 0.0"#, list.0 == List::Demonlist, ) .fetch(&mut *connection); diff --git a/pointercrate-demonlist/sql/paginate_player_ranking.sql b/pointercrate-demonlist/sql/paginate_player_ranking.sql index 98e4841f..c85908dc 100644 --- a/pointercrate-demonlist/sql/paginate_player_ranking.sql +++ b/pointercrate-demonlist/sql/paginate_player_ranking.sql @@ -1,11 +1,11 @@ -SELECT unrated_index as index, unrated_rank as rank, id, name, unrated_score as score, subdivision, iso_country_code, nation +SELECT ratedplus_index as index, ratedplus_rank as rank, id, name, ratedplus_score as score, subdivision, iso_country_code, nation FROM ranked_players -WHERE unrated_rank IS NOT NULL - AND (unrated_index < $1 OR $1 IS NULL) - AND (unrated_index > $2 OR $2 IS NULL) +WHERE ratedplus_rank IS NOT NULL + AND (ratedplus_index < $1 OR $1 IS NULL) + AND (ratedplus_index > $2 OR $2 IS NULL) AND (STRPOS(name, $3::CITEXT) > 0 OR $3 is NULL) AND (nation = $4 OR iso_country_code = $4 OR (nation IS NULL AND $5) OR ($4 IS NULL AND NOT $5)) AND (continent = CAST($6::TEXT AS continent) OR $6 IS NULL) AND (subdivision = $7 OR $7 IS NULL) -ORDER BY unrated_rank {}, id +ORDER BY ratedplus_rank {}, id LIMIT $8 \ No newline at end of file diff --git a/pointercrate-demonlist/sql/paginate_players_by_id.sql b/pointercrate-demonlist/sql/paginate_players_by_id.sql index 402b859b..c380d8f0 100644 --- a/pointercrate-demonlist/sql/paginate_players_by_id.sql +++ b/pointercrate-demonlist/sql/paginate_players_by_id.sql @@ -1,4 +1,4 @@ -SELECT players.id, players.name::TEXT, banned, nationalities.nation::TEXT, iso_country_code::TEXT, subdivision::TEXT AS iso_code, subdivisions.name AS subdivision_name, players.score, players.unrated_score, player_ranks.rank, player_ranks.unrated_rank +SELECT players.id, players.name::TEXT, banned, nationalities.nation::TEXT, iso_country_code::TEXT, subdivision::TEXT AS iso_code, subdivisions.name AS subdivision_name, players.score, players.ratedplus_score, player_ranks.rank, player_ranks.ratedplus_rank FROM players LEFT OUTER JOIN nationalities ON nationality = iso_country_code LEFT OUTER JOIN subdivisions ON iso_code = subdivision AND subdivisions.nation = nationality diff --git a/pointercrate-demonlist/src/nationality/mod.rs b/pointercrate-demonlist/src/nationality/mod.rs index 9c1594d8..d3b26884 100644 --- a/pointercrate-demonlist/src/nationality/mod.rs +++ b/pointercrate-demonlist/src/nationality/mod.rs @@ -125,14 +125,14 @@ impl Nationality { /// Updates the score for this [`Nationality`] and contained [`Subdivision`] (if set). pub async fn update_nation_score(&self, connection: &mut PgConnection) -> Result<(), sqlx::Error> { sqlx::query!( - "UPDATE nationalities SET score = coalesce(score_of_nation(true, $1), 0), unrated_score = coalesce(score_of_nation(false, $1), 0) WHERE iso_country_code = $1", + "UPDATE nationalities SET score = coalesce(score_of_nation(true, $1), 0), ratedplus_score = coalesce(score_of_nation(false, $1), 0) WHERE iso_country_code = $1", self.iso_country_code ) .execute(&mut *connection) .await?; if let Some(ref subdivision) = self.subdivision { sqlx::query!( - "UPDATE subdivisions SET score = coalesce(score_of_subdivision(true, $1, $2), 0), unrated_score = coalesce(score_of_subdivision(false, $1, $2), 0) WHERE nation = $1 AND iso_code = $2", + "UPDATE subdivisions SET score = coalesce(score_of_subdivision(true, $1, $2), 0), ratedplus_score = coalesce(score_of_subdivision(false, $1, $2), 0) WHERE nation = $1 AND iso_code = $2", self.iso_country_code, subdivision.iso_code ) diff --git a/pointercrate-demonlist/src/nationality/paginate.rs b/pointercrate-demonlist/src/nationality/paginate.rs index 796161c8..9c05717b 100644 --- a/pointercrate-demonlist/src/nationality/paginate.rs +++ b/pointercrate-demonlist/src/nationality/paginate.rs @@ -35,7 +35,7 @@ impl NationalityRankingPagination { List::Demonlist => r#"SELECT rank, score, nation, iso_country_code FROM ranked_nations WHERE rank IS NOT NULL AND (STRPOS(nation, $1::CITEXT) > 0 OR $1 is NULL) AND (continent::text = $2 OR $2 IS NULL)"#, List::RatedPlus => - r#"SELECT unrated_rank as rank, unrated_score as score, nation, iso_country_code FROM ranked_nations WHERE unrated_rank IS NOT NULL AND (STRPOS(nation, $1::CITEXT) > 0 OR $1 is NULL) AND (continent::text = $2 OR $2 IS NULL)"# + r#"SELECT ratedplus_rank as rank, ratedplus_score as score, nation, iso_country_code FROM ranked_nations WHERE ratedplus_rank IS NOT NULL AND (STRPOS(nation, $1::CITEXT) > 0 OR $1 is NULL) AND (continent::text = $2 OR $2 IS NULL)"# } ) .bind(&self.name_contains) diff --git a/pointercrate-demonlist/src/player/get.rs b/pointercrate-demonlist/src/player/get.rs index 28dc4aa8..11f2cd65 100644 --- a/pointercrate-demonlist/src/player/get.rs +++ b/pointercrate-demonlist/src/player/get.rs @@ -26,7 +26,7 @@ impl Player { pub async fn by_id(id: i32, connection: &mut PgConnection) -> Result { let result = sqlx::query!( - r#"SELECT players.id, players.name, banned, players.score, players.unrated_score, nationalities.nation::text, iso_country_code::text, iso_code::text as subdivision_code, subdivisions.name::text as subdivision_name, player_ranks.rank, player_ranks.unrated_rank FROM players LEFT OUTER JOIN nationalities ON + r#"SELECT players.id, players.name, banned, players.score, players.ratedplus_score, nationalities.nation::text, iso_country_code::text, iso_code::text as subdivision_code, subdivisions.name::text as subdivision_name, player_ranks.rank, player_ranks.ratedplus_rank FROM players LEFT OUTER JOIN nationalities ON players.nationality = nationalities.iso_country_code LEFT OUTER JOIN subdivisions ON players.subdivision = subdivisions.iso_code LEFT OUTER JOIN player_ranks ON player_ranks.id = players.id WHERE players.id = $1 AND (subdivisions.nation=nationalities.iso_country_code or players.subdivision is null)"#, id ) @@ -58,9 +58,9 @@ impl Player { banned: row.banned, }, rated_score: row.score, - score: row.unrated_score, + score: row.ratedplus_score, rated_rank: row.rank, - rank: row.unrated_rank, + rank: row.ratedplus_rank, nationality, }) }, diff --git a/pointercrate-demonlist/src/player/mod.rs b/pointercrate-demonlist/src/player/mod.rs index 556050e4..fa3dd557 100644 --- a/pointercrate-demonlist/src/player/mod.rs +++ b/pointercrate-demonlist/src/player/mod.rs @@ -89,19 +89,19 @@ impl DatabasePlayer { pub async fn update_score(&self, connection: &mut PgConnection) -> Result<(f64, f64), CoreError> { // No need to specially handle banned players - they have no approved records, so `score_of_player` will return 0 let new_scores = sqlx::query!( - "UPDATE players SET score = coalesce(score_of_player(true, $1), 0), unrated_score = coalesce(score_of_player(false, $1), 0) WHERE id = $1 RETURNING score, unrated_score", + "UPDATE players SET score = coalesce(score_of_player(true, $1), 0), ratedplus_score = coalesce(score_of_player(false, $1), 0) WHERE id = $1 RETURNING score, ratedplus_score", self.id ) .fetch_one(&mut *connection) .await?; - sqlx::query!("UPDATE nationalities SET score = coalesce(score_of_nation(true, nationalities.iso_country_code), 0), unrated_score = coalesce(score_of_nation(false, nationalities.iso_country_code), 0) FROM players WHERE players.id = $1 AND players.nationality = nationalities.iso_country_code", self.id).execute(&mut *connection).await?; - sqlx::query!("UPDATE subdivisions SET score = coalesce(score_of_subdivision(true, subdivisions.nation, subdivisions.iso_code), 0), unrated_score = coalesce(score_of_subdivision(false, subdivisions.nation, subdivisions.iso_code), 0) FROM players WHERE players.id = $1 AND players.nationality = subdivisions.nation AND players.subdivision = subdivisions.iso_code", self.id).execute(&mut *connection).await?; + sqlx::query!("UPDATE nationalities SET score = coalesce(score_of_nation(true, nationalities.iso_country_code), 0), ratedplus_score = coalesce(score_of_nation(false, nationalities.iso_country_code), 0) FROM players WHERE players.id = $1 AND players.nationality = nationalities.iso_country_code", self.id).execute(&mut *connection).await?; + sqlx::query!("UPDATE subdivisions SET score = coalesce(score_of_subdivision(true, subdivisions.nation, subdivisions.iso_code), 0), ratedplus_score = coalesce(score_of_subdivision(false, subdivisions.nation, subdivisions.iso_code), 0) FROM players WHERE players.id = $1 AND players.nationality = subdivisions.nation AND players.subdivision = subdivisions.iso_code", self.id).execute(&mut *connection).await?; sqlx::query!("REFRESH MATERIALIZED VIEW CONCURRENTLY player_ranks;") .execute(&mut *connection) .await?; - Ok((new_scores.score, new_scores.unrated_score)) + Ok((new_scores.score, new_scores.ratedplus_score)) } } diff --git a/pointercrate-demonlist/src/player/paginate.rs b/pointercrate-demonlist/src/player/paginate.rs index 1c58a640..a0a1c7d6 100644 --- a/pointercrate-demonlist/src/player/paginate.rs +++ b/pointercrate-demonlist/src/player/paginate.rs @@ -101,9 +101,9 @@ impl Paginatable for Player { banned: row.get("banned"), }, rated_score: row.get("score"), - score: row.get("unrated_score"), + score: row.get("ratedplus_score"), rated_rank: row.get("rank"), - rank: row.get("unrated_rank"), + rank: row.get("ratedplus_rank"), nationality, }) } From b611fd9e3bac484be7ac2c9aa59b3cd8a8a07934 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Fri, 24 Oct 2025 19:30:51 -0400 Subject: [PATCH 18/19] fix faulty condition in rated+ time machine --- migrations/20250722002114_unrated_levels.up.sql | 2 +- migrations/20250824144746_unrated_time_machine.up.sql | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/migrations/20250722002114_unrated_levels.up.sql b/migrations/20250722002114_unrated_levels.up.sql index d472c911..a6c2c433 100644 --- a/migrations/20250722002114_unrated_levels.up.sql +++ b/migrations/20250722002114_unrated_levels.up.sql @@ -201,7 +201,7 @@ CREATE VIEW ranked_nations AS ALTER TABLE demon_modifications ADD COLUMN rated BOOLEAN NULL DEFAULT NULL; ALTER TABLE demon_modifications ADD COLUMN rated_position SMALLINT NULL DEFAULT NULL; -UPDATE demon_modifications SET rated_position = position; +UPDATE demon_modifications SET rated_position = position WHERE position != -1; CREATE OR REPLACE FUNCTION audit_demon_modification() RETURNS trigger AS $demon_modification_trigger$ DECLARE diff --git a/migrations/20250824144746_unrated_time_machine.up.sql b/migrations/20250824144746_unrated_time_machine.up.sql index 908bf9ab..a069af97 100644 --- a/migrations/20250824144746_unrated_time_machine.up.sql +++ b/migrations/20250824144746_unrated_time_machine.up.sql @@ -21,7 +21,10 @@ FROM demons LEFT OUTER JOIN ( SELECT DISTINCT ON (id) id, CASE WHEN is_rated_list THEN rated_position ELSE position END AS position FROM demon_modifications - WHERE time >= $2 AND (is_rated_list AND rated_position IS NOT NULL) OR (NOT is_rated_list AND position != -1) + WHERE time >= $2 AND ( + (is_rated_list AND rated_position IS NOT NULL) + OR (NOT is_rated_list AND position != -1) + ) ORDER BY id, time ) t ON demons.id = t.id From dff97d15d06303a940583f2366b61a7472db0824 Mon Sep 17 00:00:00 2001 From: jasonyess Date: Fri, 7 Nov 2025 14:31:27 -0500 Subject: [PATCH 19/19] assign rate statuses for test demons --- pointercrate-test/src/demonlist.rs | 12 +++++---- pointercrate-test/tests/demonlist/demon.rs | 8 +++--- .../tests/demonlist/nationality.rs | 2 +- .../tests/demonlist/player/mod.rs | 2 +- .../tests/demonlist/player/score.rs | 26 +++++++++++++------ pointercrate-test/tests/demonlist/record.rs | 20 +++++++------- 6 files changed, 42 insertions(+), 28 deletions(-) diff --git a/pointercrate-test/src/demonlist.rs b/pointercrate-test/src/demonlist.rs index e4b6c2a0..e8807691 100644 --- a/pointercrate-test/src/demonlist.rs +++ b/pointercrate-test/src/demonlist.rs @@ -42,15 +42,17 @@ pub async fn setup_rocket(pool: Pool) -> (TestClient, PoolConnection

, position: i16, requirement: i16, verifier_id: i32, publisher_id: i32, connection: &mut PgConnection, + name: impl Into, position: i16, requirement: i16, verifier_id: i32, publisher_id: i32, rated: bool, + connection: &mut PgConnection, ) -> i32 { sqlx::query!( - "INSERT INTO demons (name, position, requirement, verifier, publisher) VALUES ($1::TEXT::CITEXT, $2, $3, $4, $5) RETURNING id", + "INSERT INTO demons (name, position, requirement, verifier, publisher, rated) VALUES ($1::TEXT::CITEXT, $2, $3, $4, $5, $6) RETURNING id", name.into(), position, requirement, verifier_id, - publisher_id + publisher_id, + rated, ) .fetch_one(&mut *connection) .await @@ -117,9 +119,9 @@ impl TestClient { pub async fn add_demon( &self, auth_context: &AuthenticatedUser, name: impl Into, position: i16, requirement: i16, - verifier: impl Into, publisher: impl Into, + verifier: impl Into, publisher: impl Into, rated: bool, ) -> FullDemon { - self.post("/api/v2/demons/", &serde_json::json!({"name": name.into(), "position": position, "requirement": requirement, "verifier": verifier.into(), "publisher": publisher.into(), "creators": []})) + self.post("/api/v2/demons/", &serde_json::json!({"name": name.into(), "position": position, "requirement": requirement, "verifier": verifier.into(), "publisher": publisher.into(), "creators": [], "rated": rated})) .expect_status(Status::Created) .authorize_as(auth_context) .get_success_result() diff --git a/pointercrate-test/tests/demonlist/demon.rs b/pointercrate-test/tests/demonlist/demon.rs index ead55bc3..fe26c05f 100644 --- a/pointercrate-test/tests/demonlist/demon.rs +++ b/pointercrate-test/tests/demonlist/demon.rs @@ -14,7 +14,7 @@ async fn test_add_demon_ratelimits(pool: Pool) { let user = pointercrate_test::user::system_user_with_perms(LIST_MODERATOR, &mut connection).await; - let demon = serde_json::json! {{"name": "Bloodbath", "requirement": 90, "position": 1, "verifier": "Riot", "publisher": "Riot", "creators": [], "level_id": 10565740}}; + let demon = serde_json::json! {{"name": "Bloodbath", "requirement": 90, "position": 1, "verifier": "Riot", "publisher": "Riot", "creators": [], "level_id": 10565740, "rated": true}}; // first one should succeed clnt.post("/api/v2/demons/", &demon) @@ -52,9 +52,9 @@ async fn test_demon_pagination(pool: Pool) { assert_eq!(links, LinksBuilder::new(URL).generate(&DemonPositionPagination::default()).unwrap()); // Let's add some data to the database and do actual tests! - let id1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 100, player.id, player.id, &mut connection).await; - let id2 = pointercrate_test::demonlist::add_demon("Bloodbath 2", 2, 100, player.id, player.id, &mut connection).await; - let id3 = pointercrate_test::demonlist::add_demon("Bloodbath 3", 3, 100, player.id, player.id, &mut connection).await; + let id1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 100, player.id, player.id, true, &mut connection).await; + let id2 = pointercrate_test::demonlist::add_demon("Bloodbath 2", 2, 100, player.id, player.id, true, &mut connection).await; + let id3 = pointercrate_test::demonlist::add_demon("Bloodbath 3", 3, 100, player.id, player.id, true, &mut connection).await; // Test only the limit parameter in isolation. Off-by-one errors in the limit are hard to catch due to how the "next" parameter // is computed internally, so make sure that if limit is ignored, at least 2 more elements would be returned diff --git a/pointercrate-test/tests/demonlist/nationality.rs b/pointercrate-test/tests/demonlist/nationality.rs index 6ef5b6c5..407c8eaf 100644 --- a/pointercrate-test/tests/demonlist/nationality.rs +++ b/pointercrate-test/tests/demonlist/nationality.rs @@ -21,7 +21,7 @@ pub async fn test_search_nation(pool: Pool) { player.set_nationality(Some(nationality), &mut connection).await.unwrap(); let helper = pointercrate_test::user::system_user_with_perms(LIST_MODERATOR, &mut connection).await; - client.add_demon(&helper, "Bloodbath", 1, 100, PLAYER_NAME, PLAYER_NAME).await; + client.add_demon(&helper, "Bloodbath", 1, 100, PLAYER_NAME, PLAYER_NAME, true).await; let json: Vec = client .get("/api/v1/nationalities/ranking/?name_contains=gErManY") diff --git a/pointercrate-test/tests/demonlist/player/mod.rs b/pointercrate-test/tests/demonlist/player/mod.rs index 309b376d..3d6d38a8 100644 --- a/pointercrate-test/tests/demonlist/player/mod.rs +++ b/pointercrate-test/tests/demonlist/player/mod.rs @@ -350,7 +350,7 @@ async fn test_player_merge(pool: Pool) { let player1 = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); let player2 = DatabasePlayer::by_name_or_create("stardust1972", &mut connection).await.unwrap(); - let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 87, player1.id, player1.id, &mut connection).await; + let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 87, player1.id, player1.id, true, &mut connection).await; pointercrate_test::demonlist::add_simple_record(90, player1.id, demon1, RecordStatus::Approved, &mut connection).await; pointercrate_test::demonlist::add_simple_record(95, player2.id, demon1, RecordStatus::Approved, &mut connection).await; diff --git a/pointercrate-test/tests/demonlist/player/score.rs b/pointercrate-test/tests/demonlist/player/score.rs index e8c9253b..d3f5e6aa 100644 --- a/pointercrate-test/tests/demonlist/player/score.rs +++ b/pointercrate-test/tests/demonlist/player/score.rs @@ -15,7 +15,9 @@ pub async fn test_score_update_on_record_update(pool: Pool) { let helper = pointercrate_test::user::system_user_with_perms(LIST_MODERATOR, &mut connection).await; let player = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); - let demon = clnt.add_demon(&helper, "Bloodbath", 1, 100, "stardust1972", "stardust1972").await; + let demon = clnt + .add_demon(&helper, "Bloodbath", 1, 100, "stardust1972", "stardust1972", true) + .await; let submission = serde_json::json! {{"progress": 100, "demon": demon.demon.base.id, "player": "stardust1971", "video": "https://youtube.com/watch?v=1234567890", "status": "Approved"}}; @@ -58,7 +60,9 @@ pub async fn test_verifications_give_score(pool: Pool) { let (clnt, mut connection) = pointercrate_test::demonlist::setup_rocket(pool).await; let helper = pointercrate_test::user::system_user_with_perms(LIST_MODERATOR, &mut connection).await; - let demon = clnt.add_demon(&helper, "Bloodbath", 1, 100, "stardust1971", "stardust1971").await; + let demon = clnt + .add_demon(&helper, "Bloodbath", 1, 100, "stardust1971", "stardust1971", true) + .await; let player: FullPlayer = clnt .get(format!("/api/v1/players/{}/", demon.demon.verifier.id)) @@ -94,7 +98,9 @@ pub async fn test_player_score_reflects_to_nationality(pool: Pool) { let (clnt, mut connection) = pointercrate_test::demonlist::setup_rocket(pool).await; let helper = pointercrate_test::user::system_user_with_perms(LIST_MODERATOR, &mut connection).await; - let demon = clnt.add_demon(&helper, "Bloodbath", 1, 100, "stardust1971", "stardust1971").await; + let demon = clnt + .add_demon(&helper, "Bloodbath", 1, 100, "stardust1971", "stardust1971", true) + .await; clnt.patch_player( demon.demon.verifier.id, @@ -140,9 +146,10 @@ pub async fn test_extended_progress_records_give_no_score(pool: Pool) for position in 1..=(list_size + 1) { last_demon_id = sqlx::query!( - "INSERT INTO demons (name, position, requirement, verifier, publisher) VALUES ('Bloodbath', $2, 98, $1, $1) RETURNING id", + "INSERT INTO demons (name, position, requirement, verifier, publisher, rated) VALUES ('Bloodbath', $2, 98, $1, $1, $3) RETURNING id", player.id, - position + position, + true, ) .fetch_one(&mut *connection) .await @@ -180,9 +187,10 @@ pub async fn test_score_resets_if_last_record_removed(pool: Pool) { for position in 1..=list_size { last_demon_id = sqlx::query!( - "INSERT INTO demons (name, position, requirement, verifier, publisher) VALUES ('Bloodbath', $2, 98, $1, $1) RETURNING id", + "INSERT INTO demons (name, position, requirement, verifier, publisher, rated) VALUES ('Bloodbath', $2, 98, $1, $1, $3) RETURNING id", player.id, - position + position, + true, ) .fetch_one(&mut *connection) .await @@ -212,7 +220,9 @@ pub async fn test_score_resets_if_last_record_removed(pool: Pool) { ); // Shift everything down. The demon on which the player has a progress record is now no longer main list, so his score should be updated to 0 now. - let _ = clnt.add_demon(&helper, "Bloodbath", 1, 100, "stardust1971", "stardust1971").await; + let _ = clnt + .add_demon(&helper, "Bloodbath", 1, 100, "stardust1971", "stardust1971", true) + .await; let record: FullRecord = clnt .get(format!("/api/v1/records/{}/", record.id)) diff --git a/pointercrate-test/tests/demonlist/record.rs b/pointercrate-test/tests/demonlist/record.rs index adb46d19..4ee6b920 100644 --- a/pointercrate-test/tests/demonlist/record.rs +++ b/pointercrate-test/tests/demonlist/record.rs @@ -83,8 +83,8 @@ async fn setup_pagination_tests(connection: &mut PgConnection) -> (i32, i32, i32 let player1 = DatabasePlayer::by_name_or_create("stardust1971", connection).await.unwrap(); let player2 = DatabasePlayer::by_name_or_create("stardust1972", connection).await.unwrap(); - let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 87, player1.id, player1.id, connection).await; - let demon2 = pointercrate_test::demonlist::add_demon("Bloodlust", 2, 53, player1.id, player1.id, connection).await; + let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 87, player1.id, player1.id, true, connection).await; + let demon2 = pointercrate_test::demonlist::add_demon("Bloodlust", 2, 53, player1.id, player1.id, true, connection).await; let r1 = pointercrate_test::demonlist::add_simple_record(100, player1.id, demon1, RecordStatus::Approved, connection).await; let r2 = pointercrate_test::demonlist::add_simple_record(70, player1.id, demon2, RecordStatus::Rejected, connection).await; @@ -99,7 +99,7 @@ async fn unauthed_submit_for_player_with_locked_submission(pool: Pool) let user = pointercrate_test::user::add_normal_user(&mut connection).await; let player1 = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); - let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 87, player1.id, player1.id, &mut connection).await; + let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 87, player1.id, player1.id, true, &mut connection).await; pointercrate_test::demonlist::put_claim(user.user().id, player1.id, true, true, &mut connection).await; @@ -123,7 +123,7 @@ async fn submit_existing_record(pool: Pool) { let (clnt, mut connection) = pointercrate_test::demonlist::setup_rocket(pool).await; let player1 = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); - let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, &mut connection).await; + let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, true, &mut connection).await; let existing = pointercrate_test::demonlist::add_simple_record(70, player1.id, demon1, RecordStatus::Approved, &mut connection).await; let submission = serde_json::json! {{"progress": 60, "demon": demon1, "player": "stardust1971", "video": "https://youtube.com/watch?v=1234567890", "raw_footage": "https://pointercrate.com"}}; @@ -142,7 +142,7 @@ async fn submit_existing_record(pool: Pool) { async fn test_submit_successful(pool: Pool) { let (clnt, mut connection) = pointercrate_test::demonlist::setup_rocket(pool).await; let player1 = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); - let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, &mut connection).await; + let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, true, &mut connection).await; let submission = serde_json::json! {{"progress": 60, "demon": demon1, "player": "stardust1971", "video": "https://youtube.com/watch?v=1234567890", "raw_footage": "https://pointercrate.com"}}; @@ -159,7 +159,7 @@ async fn test_no_submitter_info_on_unauthed_get(pool: Pool) { let (clnt, mut connection) = pointercrate_test::demonlist::setup_rocket(pool).await; let player1 = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); - let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, &mut connection).await; + let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, true, &mut connection).await; let existing = pointercrate_test::demonlist::add_simple_record(70, player1.id, demon1, RecordStatus::Approved, &mut connection).await; let record: FullRecord = clnt.get(format!("/api/v1/records/{}/", existing)).get_success_result().await; @@ -175,7 +175,7 @@ async fn test_no_raw_footage_on_unauthed_get(pool: Pool) { let user = pointercrate_test::user::system_user_with_perms(LIST_HELPER, &mut connection).await; let player1 = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); - let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, &mut connection).await; + let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, true, &mut connection).await; let submission = serde_json::json! {{"progress": 100, "demon": demon1, "player": player1.name, "video": "https://youtube.com/watch?v=1234567890", "raw_footage": raw_footage, "status": "approved"}}; let record: FullRecord = clnt @@ -202,7 +202,7 @@ async fn test_record_note_creation_and_deletion(pool: Pool) { let helper = system_user_with_perms(LIST_HELPER, &mut connection).await; let player1 = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); - let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, &mut connection).await; + let demon1 = pointercrate_test::demonlist::add_demon("Bloodbath", 1, 50, player1.id, player1.id, true, &mut connection).await; let record = add_simple_record(100, player1.id, demon1, RecordStatus::Approved, &mut connection).await; // Create a record note whose author is `helper`. @@ -235,7 +235,9 @@ async fn test_record_deletion_updates_player_score(pool: Pool) { let helper = pointercrate_test::user::system_user_with_perms(LIST_MODERATOR, &mut connection).await; let player = DatabasePlayer::by_name_or_create("stardust1971", &mut connection).await.unwrap(); - let demon = clnt.add_demon(&helper, "Bloodbath", 1, 100, "stardust1972", "stardust1972").await; + let demon = clnt + .add_demon(&helper, "Bloodbath", 1, 100, "stardust1972", "stardust1972", true) + .await; let submission = serde_json::json! {{"progress": 100, "demon": demon.demon.base.id, "player": "stardust1971", "video": "https://youtube.com/watch?v=1234567890", "status": "Approved"}};