1
0
mirror of https://github.com/jcwimer/wrestlingApp synced 2026-03-26 09:43:51 +00:00

29 Commits

Author SHA1 Message Date
7526148ba5 Better printing for brackets 2026-03-05 18:10:22 -05:00
e8e0fa291b Added clarifying documentation for BYE points as well as fixed logic. 2026-03-04 18:17:26 -05:00
679fc2fcb9 Ensuring good caching for the most popular pages. Added tests. 2026-03-02 18:34:12 -05:00
18d39c6c8f Using eager loading in GenerateTournamentMatches and AdvanceWrestler, generating/manipulating in-memory, and doing a single bulk insert or update at the end. 2026-02-24 20:58:36 -05:00
ca4d5ce0db Use websockets on stats page to determine which match to stat. 2026-02-23 17:56:40 -05:00
654cb84827 Use turbo streams for the bout board instead of auto refreshing every 30 seconds. 2026-02-20 19:20:33 -05:00
dc50efe8fc Removed the use of datatables and added pagination and search on all_matches. 2026-02-19 17:53:40 -05:00
8670ce38c3 Fixed a number of N+1 issues on low traffic pages. I also added relevant html tests for these pages. 2026-02-17 22:27:11 -05:00
d359be3ea1 Fixed deprecations 2026-02-13 18:02:04 -05:00
e97aa0d680 Fixed N+1 on up_matches and added html cache. 2026-02-13 18:02:04 -05:00
ae8d995b2c Added a QR code page that generates a QR code for tournament directors to print out. 2026-02-11 18:23:14 -05:00
d57aaac09d Hide ads on schools#show, wrestlers#new, wrestlers#edit, and mats#show 2026-02-11 07:55:49 -05:00
fcc8a9b9a9 Updated to ruby 4.0.1 2026-02-10 17:58:22 -05:00
b51866e9d8 Added tests for hiding ads on lineup submission. 2026-02-09 18:36:56 -05:00
07d43e7720 Hide ads for coaches when submitting lineups 2026-02-08 18:59:42 -05:00
d8b6cfa8ac Added bundle audit to pipeline. 2026-02-05 18:40:38 -05:00
5d674f894f Added round number and bracket position under the bout number 2026-02-04 18:16:19 -05:00
25df2a7280 Updated to rails 8.1.2. 2026-02-04 18:16:19 -05:00
2767274066 Added queues for mats and provided a way for tournament directors to move matches to a mat. 2026-02-03 17:50:52 -05:00
a2f8c7bced Stats page should auto push stats when it reconnects to the websocket. Spectate page should auto pull when it reconnects to the websocket. 2026-01-29 17:28:14 -05:00
9c2a9d62ad Fixed random seeding for double elimination. Since bracket positions are already evenly distributed on top half and bottom half of the bracket, I only need to pick odd or even bracket line numbers. 2026-01-23 17:35:16 -05:00
556090c16b Fixed double elimination generate loser names for a 6 man bracket when we're placing top 8 2026-01-23 17:35:16 -05:00
86f9c03991 Fixed double elim match generation errors and added tests 2026-01-23 17:35:16 -05:00
c8764c149b Added back tournament import text for the development environment 2026-01-23 17:35:16 -05:00
fe9a9c628c Fix arguements for the tournament backup and import jobs 2026-01-22 16:59:44 -05:00
7e4b6d8fc8 Fix round 1 bracket name when the first round of the bracket is not the first round of the tournament 2026-01-19 23:25:15 -05:00
940f7b1d00 Job concurrency per tournament is 1 so we don't have to scale too much on active queue. Pages no longer refresh automatically after navigating away from the bout board. Tournament backups are no longer deleted when restoring from a backup. Cloudflare is blocking manual backup imports so I have deleted that form on the backups page. 2026-01-16 18:21:17 -05:00
52df73d14f Fixed random double elimination seeding to avoid double byes in round 1 and evenly distribute the number of round 1 matches from the top and bottom half of the bracket 2026-01-14 19:00:35 -05:00
8b03a74b1e Fixed the save seeds button on weights#show to work on mobile. Fixed the trashcan and edit icons on tournaments#show schools#show and weights#show to work on mobile. Destroy all tournament backups on tournament cleanup. Added bracket position to bout board. 2026-01-13 17:02:59 -05:00
126 changed files with 4781 additions and 1534 deletions

View File

@@ -1 +1 @@
wrestlingdev
wrestlingdev

View File

@@ -1 +1 @@
ruby-3.2.0
ruby-4.0.1

View File

@@ -1,8 +1,8 @@
source 'https://rubygems.org'
ruby '3.2.0'
ruby '4.0.1'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '8.0.3'
gem 'rails', '8.1.2'
# Added in rails 7.1
gem 'rails-html-sanitizer'
@@ -67,6 +67,7 @@ gem 'influxdb-rails'
gem 'cancancan'
gem 'round_robin_tournament'
gem 'rb-readline'
gem 'rqrcode'
# Replacing Delayed Job with Solid Queue
# gem 'delayed_job_active_record'
gem 'solid_queue'
@@ -91,4 +92,3 @@ group :development, :test do
# rails-controller-testing is needed for assert_template
gem 'rails-controller-testing'
end

View File

@@ -1,29 +1,31 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.3)
actionpack (= 8.0.3)
activesupport (= 8.0.3)
action_text-trix (2.1.16)
railties
actioncable (8.1.2)
actionpack (= 8.1.2)
activesupport (= 8.1.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.3)
actionpack (= 8.0.3)
activejob (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
actionmailbox (8.1.2)
actionpack (= 8.1.2)
activejob (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
mail (>= 2.8.0)
actionmailer (8.0.3)
actionpack (= 8.0.3)
actionview (= 8.0.3)
activejob (= 8.0.3)
activesupport (= 8.0.3)
actionmailer (8.1.2)
actionpack (= 8.1.2)
actionview (= 8.1.2)
activejob (= 8.1.2)
activesupport (= 8.1.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.3)
actionview (= 8.0.3)
activesupport (= 8.0.3)
actionpack (8.1.2)
actionview (= 8.1.2)
activesupport (= 8.1.2)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -31,42 +33,43 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.3)
actionpack (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
actiontext (8.1.2)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.3)
activesupport (= 8.0.3)
actionview (8.1.2)
activesupport (= 8.1.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.3)
activesupport (= 8.0.3)
activejob (8.1.2)
activesupport (= 8.1.2)
globalid (>= 0.3.6)
activemodel (8.0.3)
activesupport (= 8.0.3)
activerecord (8.0.3)
activemodel (= 8.0.3)
activesupport (= 8.0.3)
activemodel (8.1.2)
activesupport (= 8.1.2)
activerecord (8.1.2)
activemodel (= 8.1.2)
activesupport (= 8.1.2)
timeout (>= 0.4.0)
activestorage (8.0.3)
actionpack (= 8.0.3)
activejob (= 8.0.3)
activerecord (= 8.0.3)
activesupport (= 8.0.3)
activestorage (8.1.2)
actionpack (= 8.1.2)
activejob (= 8.1.2)
activerecord (= 8.1.2)
activesupport (= 8.1.2)
marcel (~> 1.0)
activesupport (8.0.3)
activesupport (8.1.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
@@ -74,39 +77,39 @@ GEM
uri (>= 0.13.1)
ast (2.4.3)
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.4.1)
bigdecimal (3.3.0)
bootsnap (1.18.6)
bcrypt (3.1.21)
bigdecimal (4.0.1)
bootsnap (1.23.0)
msgpack (~> 1.2)
brakeman (7.1.0)
brakeman (8.0.2)
racc
builder (3.3.0)
bullet (8.0.8)
bullet (8.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3)
bundler-audit (0.9.3)
bundler (>= 1.2.0)
thor (~> 1.0)
cancancan (3.6.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
chunky_png (1.4.0)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
crass (1.0.6)
daemons (1.4.1)
date (3.4.1)
date (3.5.1)
drb (2.2.3)
erb (5.0.3)
erb (6.0.1)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
fugit (1.11.2)
et-orbi (~> 1, >= 1.2.11)
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
i18n (1.14.7)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
importmap-rails (2.2.2)
importmap-rails (2.2.3)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
@@ -114,33 +117,36 @@ GEM
influxdb-rails (1.0.3)
influxdb (~> 0.6, >= 0.6.4)
railties (>= 5.0)
io-console (0.8.1)
irb (1.15.2)
io-console (0.8.2)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.14.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
jquery-rails (4.6.0)
jquery-rails (4.6.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.15.1)
json (2.18.1)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.1)
loofah (2.25.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mail (2.9.0)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.1.0)
mini_mime (1.1.5)
minitest (5.25.5)
minitest (6.0.1)
prism (~> 1.5)
mission_control-jobs (1.1.0)
actioncable (>= 7.1)
actionpack (>= 7.1)
@@ -151,12 +157,12 @@ GEM
railties (>= 7.1)
stimulus-rails
turbo-rails
mocha (2.7.1)
mocha (3.0.2)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
mysql2 (0.5.7)
bigdecimal
net-imap (0.5.12)
net-imap (0.6.3)
date
net-protocol
net-pop (0.1.2)
@@ -165,64 +171,64 @@ GEM
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.10-aarch64-linux-gnu)
nio4r (2.7.5)
nokogiri (1.19.0-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl)
nokogiri (1.19.0-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu)
nokogiri (1.19.0-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
nokogiri (1.19.0-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
nokogiri (1.19.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
nokogiri (1.19.0-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
nokogiri (1.19.0-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl)
nokogiri (1.19.0-x86_64-linux-musl)
racc (~> 1.4)
parallel (1.27.0)
parser (3.3.9.0)
parser (3.3.10.1)
ast (~> 2.4.1)
racc
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.5.1)
prism (1.9.0)
propshaft (1.3.1)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.2.6)
psych (5.3.1)
date
stringio
puma (7.0.4)
puma (7.2.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.2)
rack (3.2.4)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rackup (2.3.1)
rack (>= 3)
rails (8.0.3)
actioncable (= 8.0.3)
actionmailbox (= 8.0.3)
actionmailer (= 8.0.3)
actionpack (= 8.0.3)
actiontext (= 8.0.3)
actionview (= 8.0.3)
activejob (= 8.0.3)
activemodel (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
rails (8.1.2)
actioncable (= 8.1.2)
actionmailbox (= 8.1.2)
actionmailer (= 8.1.2)
actionpack (= 8.1.2)
actiontext (= 8.1.2)
actionview (= 8.1.2)
activejob (= 8.1.2)
activemodel (= 8.1.2)
activerecord (= 8.1.2)
activestorage (= 8.1.2)
activesupport (= 8.1.2)
bundler (>= 1.15.0)
railties (= 8.0.3)
railties (= 8.1.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -239,9 +245,9 @@ GEM
rails_stdout_logging
rails_serve_static_assets (0.0.5)
rails_stdout_logging (0.0.5)
railties (8.0.3)
actionpack (= 8.0.3)
activesupport (= 8.0.3)
railties (8.1.2)
actionpack (= 8.1.2)
activesupport (= 8.1.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -249,17 +255,21 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.0)
rake (13.3.1)
rb-readline (0.5.5)
rdoc (6.15.0)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
tsort
regexp_parser (2.11.3)
reline (0.6.2)
reline (0.6.3)
io-console (~> 0.5)
round_robin_tournament (0.1.2)
rubocop (1.81.1)
rqrcode (3.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.1.0)
rubocop (1.84.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -267,15 +277,15 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.47.1)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
prism (~> 1.7)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sdoc (2.6.4)
sdoc (2.6.5)
rdoc (>= 5.0)
securerandom (0.4.1)
solid_cable (3.0.12)
@@ -283,50 +293,50 @@ GEM
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_cache (1.0.7)
solid_cache (1.0.10)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.2.1)
solid_queue (1.3.1)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11.0)
fugit (~> 1.11)
railties (>= 7.1)
thor (>= 1.3.1)
spring (4.4.0)
sqlite3 (2.7.4-aarch64-linux-gnu)
sqlite3 (2.7.4-aarch64-linux-musl)
sqlite3 (2.7.4-arm-linux-gnu)
sqlite3 (2.7.4-arm-linux-musl)
sqlite3 (2.7.4-arm64-darwin)
sqlite3 (2.7.4-x86_64-darwin)
sqlite3 (2.7.4-x86_64-linux-gnu)
sqlite3 (2.7.4-x86_64-linux-musl)
spring (4.4.2)
sqlite3 (2.9.0-aarch64-linux-gnu)
sqlite3 (2.9.0-aarch64-linux-musl)
sqlite3 (2.9.0-arm-linux-gnu)
sqlite3 (2.9.0-arm-linux-musl)
sqlite3 (2.9.0-arm64-darwin)
sqlite3 (2.9.0-x86_64-darwin)
sqlite3 (2.9.0-x86_64-linux-gnu)
sqlite3 (2.9.0-x86_64-linux-musl)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.7)
thor (1.4.0)
timeout (0.4.3)
stringio (3.2.0)
thor (1.5.0)
timeout (0.6.0)
tsort (0.2.0)
turbo-rails (2.0.17)
turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2025.2)
tzinfo-data (1.2025.3)
tzinfo (>= 1.0.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
unicode-emoji (4.2.0)
uniform_notifier (1.18.0)
uri (1.0.4)
uri (1.1.1)
useragent (0.16.11)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.7.3)
zeitwerk (2.7.4)
PLATFORMS
aarch64-linux-gnu
@@ -355,12 +365,13 @@ DEPENDENCIES
mysql2
propshaft
puma
rails (= 8.0.3)
rails (= 8.1.2)
rails-controller-testing
rails-html-sanitizer
rails_12factor
rb-readline
round_robin_tournament
rqrcode
rubocop
sdoc
solid_cable
@@ -373,7 +384,7 @@ DEPENDENCIES
tzinfo-data
RUBY VERSION
ruby 3.2.0p0
ruby 4.0.1p0
BUNDLED WITH
2.6.9
4.0.3

View File

@@ -7,8 +7,8 @@ This application is being created to run a wrestling tournament.
**Public Production Url:** [https://wrestlingdev.com](https://wrestlingdev.com)
**App Info**
* Ruby 3.2.0
* Rails 8.0.2
* Ruby 4.0.1
* Rails 8.1.2
* DB MySQL/MariaDB
* Solid Cache -> MySQL/MariaDB for html partial caching
* Solid Queue -> MySQL/MariaDB for background job processing
@@ -34,11 +34,14 @@ In development environments, background jobs run inline (synchronously) by defau
To run a single test file:
1. Get a shell with ruby and rails: `bash bin/rails-dev-run.sh wrestlingdev-development`
2. `rake test TEST=test/models/match_test.rb`
2. `rake test TEST=test/models/match_test.rb` OR `rails test test/models/match_test.rb`
To run a single test inside a file:
1. Get a shell with ruby and rails: `bash bin/rails-dev-run.sh wrestlingdev-development`
2. `rake test TEST=test/models/match_test.rb TESTOPTS="--name='/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/'"`
2. `rake test TEST=test/models/match_test.rb TESTOPTS="--name='/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/'"` OR `rails test test/models/match_test.rb --name=/test_Match_should_not_be_valid_if_an_incorrect_win_type_is_given/`
To run tests in verbose mode (outputs the time for each test file and the test file name)
`rails test -v`
## Develop with rvm
With rvm installed, run `rvm install ruby-3.2.0`
@@ -149,7 +152,7 @@ SolidQueue plugin enabled in Puma
```
I have deployed Mission Control as a UI for SolidQueue. The uri for mission control is `/jobs`.
For the development environment, the user/password is dev/secret. For the production environment, it is defined by environment variables.
For the development environment, the user/password is dev/secret. For the production environment, it is defined by environment variables WRESTLINGDEV_MISSION_CONTROL_USER/WRESTLINGDEV_MISSION_CONTROL_PASSWORD. You can see this in `config/environments/production.rb` and `config/environments/development.rb`.
## Environment Variables

View File

@@ -5,7 +5,6 @@ import "@hotwired/turbo-rails";
import { createConsumer } from "@rails/actioncable"; // Import createConsumer directly
import "jquery";
import "bootstrap";
import "datatables.net";
// Stimulus setup
import { Application } from "@hotwired/stimulus";
@@ -20,12 +19,14 @@ import WrestlerColorController from "controllers/wrestler_color_controller";
import MatchScoreController from "controllers/match_score_controller";
import MatchDataController from "controllers/match_data_controller";
import MatchSpectateController from "controllers/match_spectate_controller";
import UpMatchesConnectionController from "controllers/up_matches_connection_controller";
// Register controllers
application.register("wrestler-color", WrestlerColorController);
application.register("match-score", MatchScoreController);
application.register("match-data", MatchDataController);
application.register("match-spectate", MatchSpectateController);
application.register("up-matches-connection", UpMatchesConnectionController);
// Your existing Action Cable consumer setup
(function() {
@@ -39,9 +40,9 @@ application.register("match-spectate", MatchSpectateController);
}
}).call(this);
console.log("Propshaft/Importmap application.js initialized with jQuery, Bootstrap, Stimulus, and DataTables.");
console.log("Propshaft/Importmap application.js initialized with jQuery, Bootstrap, and Stimulus.");
// If you have custom JavaScript files in app/javascript/ that were previously
// handled by Sprockets `require_tree`, you'll need to import them here explicitly.
// For example:
// import "./my_custom_logic";
// import "./my_custom_logic";

View File

@@ -13,6 +13,8 @@ export default class extends Controller {
connect() {
console.log("Match data controller connected")
this.isConnected = false
this.pendingLocalSync = { w1: false, w2: false }
this.w1 = {
name: "w1",
@@ -69,6 +71,7 @@ export default class extends Controller {
wrestler.updated_at = new Date().toISOString()
this.updateHtmlValues()
this.saveToLocalStorage(wrestler)
if (!this.isConnected) this.pendingLocalSync[wrestler.name] = true
// Send the update via Action Cable if subscribed
if (this.matchSubscription) {
@@ -109,6 +112,7 @@ export default class extends Controller {
// Update the internal JS object
wrestler.stats = newValue
wrestler.updated_at = new Date().toISOString()
if (!this.isConnected) this.pendingLocalSync[wrestler.name] = true
// Save to localStorage
this.saveToLocalStorage(wrestler)
@@ -334,15 +338,18 @@ export default class extends Controller {
{
connected: () => {
console.log(`[Stats AC] Connected to MatchStatsChannel for match ID: ${matchId}`)
this.isConnected = true
if (this.statusIndicatorTarget) {
this.statusIndicatorTarget.innerText = "Connected: Stats will update in real-time."
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-warning', 'alert-danger')
this.statusIndicatorTarget.classList.add('alert-success')
}
this.sendCurrentStatsOnReconnect()
},
disconnected: () => {
console.log(`[Stats AC] Disconnected from MatchStatsChannel`)
this.isConnected = false
if (this.statusIndicatorTarget) {
this.statusIndicatorTarget.innerText = "Disconnected: Stats updates paused."
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-danger')
@@ -356,15 +363,25 @@ export default class extends Controller {
// Update w1 stats
if (data.w1_stat !== undefined && this.w1StatTarget) {
console.log(`[Stats AC] Updating w1_stat: ${data.w1_stat.substring(0, 30)}...`)
this.w1.stats = data.w1_stat
this.w1StatTarget.value = data.w1_stat
if (!this.pendingLocalSync.w1 || data.w1_stat === this.w1.stats) {
this.w1.stats = data.w1_stat
this.w1StatTarget.value = data.w1_stat
this.pendingLocalSync.w1 = false
} else {
console.log('[Stats AC] Skipping w1_stat overwrite due to pending local changes.')
}
}
// Update w2 stats
if (data.w2_stat !== undefined && this.w2StatTarget) {
console.log(`[Stats AC] Updating w2_stat: ${data.w2_stat.substring(0, 30)}...`)
this.w2.stats = data.w2_stat
this.w2StatTarget.value = data.w2_stat
if (!this.pendingLocalSync.w2 || data.w2_stat === this.w2.stats) {
this.w2.stats = data.w2_stat
this.w2StatTarget.value = data.w2_stat
this.pendingLocalSync.w2 = false
} else {
console.log('[Stats AC] Skipping w2_stat overwrite due to pending local changes.')
}
}
},
@@ -381,4 +398,23 @@ export default class extends Controller {
}
)
}
}
sendCurrentStatsOnReconnect() {
if (!this.matchSubscription) return
const payload = {}
if (typeof this.w1?.stats === 'string' && this.w1.stats.length > 0) {
payload.new_w1_stat = this.w1.stats
this.pendingLocalSync.w1 = true
}
if (typeof this.w2?.stats === 'string' && this.w2.stats.length > 0) {
payload.new_w2_stat = this.w2.stats
this.pendingLocalSync.w2 = true
}
if (Object.keys(payload).length > 0) {
console.log('[ActionCable] Reconnect sync: sending current stats payload:', payload)
this.matchSubscription.perform('send_stat', payload)
} else {
console.log('[ActionCable] Reconnect sync: no local stats to send.')
}
}
}

View File

@@ -76,6 +76,11 @@ export default class extends Controller {
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-secondary', 'text-danger', 'text-dark')
this.statusIndicatorTarget.classList.add('alert-success')
}
try {
this.matchSubscription.perform('request_sync')
} catch (e) {
console.error('[Spectator AC] request_sync perform failed:', e)
}
},
disconnected: () => {
console.log(`[Spectator AC Callback] Disconnected: ${matchId}`)
@@ -131,4 +136,4 @@ export default class extends Controller {
this.finishedTarget.textContent = data.finished ? 'Yes' : 'No'
}
}
}
}

View File

@@ -0,0 +1,70 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["stream", "statusIndicator"]
connect() {
this.setupSubscription()
}
disconnect() {
this.cleanupSubscription()
}
setupSubscription() {
this.cleanupSubscription()
this.setStatus("Connecting to server for real-time bout board updates...", "info")
if (!this.hasStreamTarget) {
this.setStatus("Error: Stream source not found.", "danger")
return
}
const signedStreamName = this.streamTarget.getAttribute("signed-stream-name")
if (!signedStreamName) {
this.setStatus("Error: Invalid stream source.", "danger")
return
}
if (!window.App || !window.App.cable) {
this.setStatus("Error: WebSockets unavailable. Bout board won't update in real-time. Refresh the page to update.", "danger")
return
}
this.subscription = App.cable.subscriptions.create(
{
channel: "Turbo::StreamsChannel",
signed_stream_name: signedStreamName
},
{
connected: () => {
this.setStatus("Connected: Bout board updating in real-time.", "success")
},
disconnected: () => {
this.setStatus("Disconnected: Live bout board updates paused.", "warning")
},
rejected: () => {
this.setStatus("Error: Live bout board connection rejected.", "danger")
}
}
)
}
cleanupSubscription() {
if (!this.subscription) return
this.subscription.unsubscribe()
this.subscription = null
}
setStatus(message, type) {
if (!this.hasStatusIndicatorTarget) return
this.statusIndicatorTarget.innerText = message
this.statusIndicatorTarget.classList.remove("alert-secondary", "alert-info", "alert-success", "alert-warning", "alert-danger")
if (type === "success") this.statusIndicatorTarget.classList.add("alert-success")
else if (type === "warning") this.statusIndicatorTarget.classList.add("alert-warning")
else if (type === "danger") this.statusIndicatorTarget.classList.add("alert-danger")
else this.statusIndicatorTarget.classList.add("alert-info")
}
}

View File

@@ -60,4 +60,29 @@ class MatchChannel < ApplicationCable::Channel
Rails.logger.info "[MatchChannel] No new stat data provided in send_stat for match #{@match.id}, not updating DB or broadcasting."
end
end
# Called when client wants the latest stats immediately after reconnect
def request_sync
unless @match
Rails.logger.error "[MatchChannel] Error: request_sync called but @match is nil. Client params on sub: #{params[:match_id]}"
return
end
payload = {
w1_stat: @match.w1_stat,
w2_stat: @match.w2_stat,
score: @match.score,
win_type: @match.win_type,
winner_name: @match.winner&.name,
winner_id: @match.winner_id,
finished: @match.finished
}.compact
if payload.present?
Rails.logger.info "[MatchChannel] request_sync transmit for match #{@match.id} with payload: #{payload.inspect}"
transmit(payload)
else
Rails.logger.info "[MatchChannel] request_sync payload empty for match #{@match.id}, not transmitting."
end
end
end

View File

@@ -4,7 +4,7 @@ class MatAssignmentRulesController < ApplicationController
before_action :set_mat_assignment_rule, only: [:edit, :update, :destroy]
def index
@mat_assignment_rules = @tournament.mat_assignment_rules
@mat_assignment_rules = @tournament.mat_assignment_rules.includes(:mat)
@weights_by_id = @tournament.weights.index_by(&:id) # For quick lookup
end

View File

@@ -1,6 +1,6 @@
class MatchesController < ApplicationController
before_action :set_match, only: [:show, :edit, :update, :stat, :spectate]
before_action :check_access, only: [:edit,:update, :stat]
before_action :set_match, only: [:show, :edit, :update, :stat, :spectate, :edit_assignment, :update_assignment]
before_action :check_access, only: [:edit, :update, :stat, :edit_assignment, :update_assignment]
# GET /matches/1
# GET /matches/1.json
@@ -21,7 +21,7 @@ class MatchesController < ApplicationController
session[:return_path] = "/tournaments/#{@match.tournament.id}/matches"
end
def stat
def stat
# @show_next_bout_button = false
if params[:match]
@match = Match.where(:id => params[:match]).includes(:wrestlers).first
@@ -50,8 +50,21 @@ class MatchesController < ApplicationController
end
@tournament = @match.tournament
end
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
session[:error_return_path] = "/matches/#{@match.id}/stat"
if @match&.mat
@mat = @match.mat
queue_position = @mat.queue_position_for_match(@match)
@next_match = queue_position == 1 ? @mat.queue2_match : nil
@show_next_bout_button = queue_position == 1
if request.referer&.include?("/tournaments/#{@tournament.id}/matches")
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
else
session[:return_path] = mat_path(@mat)
end
session[:error_return_path] = "/matches/#{@match.id}/stat"
else
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
session[:error_return_path] = "/matches/#{@match.id}/stat"
end
end
# GET /matches/:id/spectate
@@ -71,6 +84,49 @@ class MatchesController < ApplicationController
end
end
# GET /matches/1/edit_assignment
def edit_assignment
@tournament = @match.tournament
@mats = @tournament.mats.sort_by(&:name)
@current_mat = @match.mat
@current_queue_position = @current_mat&.queue_position_for_match(@match)
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
end
# PATCH /matches/1/update_assignment
def update_assignment
@tournament = @match.tournament
mat_id = params.dig(:match, :mat_id)
queue_position = params.dig(:match, :queue_position)
if mat_id.blank?
Mat.where("queue1 = :match_id OR queue2 = :match_id OR queue3 = :match_id OR queue4 = :match_id", match_id: @match.id)
.find_each { |mat| mat.remove_match_from_queue_and_collapse!(@match.id) }
@match.update(mat_id: nil)
redirect_to session.delete(:return_path) || "/tournaments/#{@tournament.id}/matches", notice: "Match assignment cleared."
return
end
if queue_position.blank?
redirect_to edit_assignment_match_path(@match), alert: "Queue position is required when selecting a mat."
return
end
unless %w[1 2 3 4].include?(queue_position.to_s)
redirect_to edit_assignment_match_path(@match), alert: "Queue position must be between 1 and 4."
return
end
mat = @tournament.mats.find_by(id: mat_id)
unless mat
redirect_to edit_assignment_match_path(@match), alert: "Selected mat was not found."
return
end
mat.assign_match_to_queue!(@match, queue_position)
redirect_to session.delete(:return_path) || "/tournaments/#{@tournament.id}/matches", notice: "Match assignment updated."
end
# PATCH/PUT /matches/1
# PATCH/PUT /matches/1.json
def update

View File

@@ -1,22 +1,21 @@
class MatsController < ApplicationController
before_action :set_mat, only: [:show, :edit, :update, :destroy, :assign_next_match]
before_action :check_access, only: [:new,:create,:update,:destroy,:edit,:show, :assign_next_match]
before_action :check_for_matches, only: [:show]
# GET /mats/1
# GET /mats/1.json
def show
bout_number_param = params[:bout_number] # Read the bout_number from the URL params
if bout_number_param
@show_next_bout_button = false
@match = @mat.unfinished_matches.find { |m| m.bout_number == bout_number_param.to_i }
bout_number_param = params[:bout_number]
@queue_matches = @mat.queue_matches
@match = if bout_number_param
@queue_matches.compact.find { |m| m.bout_number == bout_number_param.to_i }
else
@show_next_bout_button = true
@match = @mat.unfinished_matches.first
@queue_matches[0]
end
@next_match = @mat.unfinished_matches.second # Second unfinished match on the mat
# If a requested bout is no longer queued, fall back to queue1.
@match ||= @queue_matches[0]
@next_match = @queue_matches[1]
@show_next_bout_button = false
@wrestlers = []
if @match
@@ -82,8 +81,8 @@ class MatsController < ApplicationController
def assign_next_match
@tournament = @mat.tournament_id
respond_to do |format|
if @mat.assign_next_match
format.html { redirect_to "/tournaments/#{@mat.tournament.id}", notice: "Next Match on Mat #{@mat.name} successfully completed." }
if @mat.advance_queue!
format.html { redirect_to "/tournaments/#{@mat.tournament.id}", notice: "Mat #{@mat.name} queue advanced." }
format.json { head :no_content }
else
format.html { redirect_to "/tournaments/#{@mat.tournament.id}", alert: "There was an error." }
@@ -142,11 +141,4 @@ class MatsController < ApplicationController
end
def check_for_matches
if @mat
if @mat.tournament.matches.empty?
redirect_to "/tournaments/#{@tournament.id}/no_matches"
end
end
end
end

View File

@@ -1,11 +1,11 @@
class StaticPagesController < ApplicationController
def my_tournaments
tournaments_created = current_user.tournaments
tournaments_delegated = current_user.delegated_tournaments
tournaments_created = current_user.tournaments.to_a
tournaments_delegated = current_user.delegated_tournaments.to_a
all_tournaments = tournaments_created + tournaments_delegated
@tournaments = all_tournaments.sort_by{|t| t.days_until_start}
@schools = current_user.delegated_schools
@schools = current_user.delegated_schools.includes(:tournament)
end
def not_allowed

View File

@@ -1,13 +1,13 @@
class TournamentsController < ApplicationController
before_action :set_tournament, only: [:all_results, :delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:bout_sheets,:swap,:weigh_in_sheet,:error,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:remove_delegate,:school_delegate,:delegate,:matches,:weigh_in,:weigh_in_weight,:create_custom_weights,:show,:edit,:update,:destroy,:up_matches,:no_matches,:team_scores,:generate_matches,:bracket,:all_brackets]
before_action :check_access_manage, only: [:delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches]
before_action :set_tournament, only: [:all_results, :delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:bout_sheets,:swap,:weigh_in_sheet,:error,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:remove_delegate,:school_delegate,:delegate,:matches,:weigh_in,:weigh_in_weight,:create_custom_weights,:show,:edit,:update,:destroy,:up_matches,:no_matches,:team_scores,:generate_matches,:bracket,:all_brackets,:qrcode]
before_action :check_access_manage, only: [:delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches,:qrcode]
before_action :check_access_destroy, only: [:destroy,:delegate,:remove_delegate]
before_action :check_tournament_errors, only: [:generate_matches]
before_action :check_for_matches, only: [:all_results,:up_matches,:bracket,:all_brackets]
before_action :check_for_matches, only: [:all_results,:bracket,:all_brackets]
before_action :check_access_read, only: [:all_results,:up_matches,:bracket,:all_brackets]
def weigh_in_sheet
@schools = @tournament.schools.includes(wrestlers: :weight)
end
def calculate_team_scores
@@ -92,12 +92,9 @@ class TournamentsController < ApplicationController
end
end
end
@users_delegates = []
@tournament.schools.each do |s|
s.delegates.each do |d|
@users_delegates << d
end
end
@users_delegates = SchoolDelegate.includes(:user, :school)
.joins(:school)
.where(schools: { tournament_id: @tournament.id })
end
def delegate
@@ -115,11 +112,63 @@ class TournamentsController < ApplicationController
end
end
end
@users_delegates = @tournament.delegates
@users_delegates = @tournament.delegates.includes(:user)
end
def matches
@matches = @tournament.matches.includes(:wrestlers,:schools).sort_by{|m| m.bout_number}
per_page = 50
@page = params[:page].to_i > 0 ? params[:page].to_i : 1
offset = (@page - 1) * per_page
matches_table = Match.arel_table
matches_scope = @tournament.matches.order(:bout_number)
if params[:search].present?
wrestlers_table = Wrestler.arel_table
schools_table = School.arel_table
search_terms = params[:search].downcase.split
search_terms.each do |term|
escaped_term = ActiveRecord::Base.sanitize_sql_like(term)
pattern = "%#{escaped_term}%"
matching_wrestler_ids = Wrestler
.joins(:weight)
.left_outer_joins(:school)
.where(weights: { tournament_id: @tournament.id })
.where(
wrestlers_table[:name].matches(pattern)
.or(schools_table[:name].matches(pattern))
)
.distinct
.select(:id)
term_scope = @tournament.matches.where(w1: matching_wrestler_ids)
.or(@tournament.matches.where(w2: matching_wrestler_ids))
if term.match?(/\A\d+\z/)
term_scope = term_scope.or(@tournament.matches.where(bout_number: term.to_i))
end
matches_scope = matches_scope.where(id: term_scope.select(:id))
end
end
@total_count = matches_scope.count
@total_pages = (@total_count / per_page.to_f).ceil
@per_page = per_page
loser1_not_bye = matches_table[:loser1_name].not_eq("BYE").or(matches_table[:loser1_name].eq(nil))
loser2_not_bye = matches_table[:loser2_name].not_eq("BYE").or(matches_table[:loser2_name].eq(nil))
non_bye_scope = matches_scope.where(loser1_not_bye).where(loser2_not_bye)
@matches_without_byes_count = non_bye_scope.count
@unfinished_matches_without_byes_count = non_bye_scope.where(finished: [nil, 0]).count
@matches = matches_scope
.includes({ wrestler1: :school }, { wrestler2: :school }, { weight: :matches })
.offset(offset)
.limit(per_page)
if @match
@w1 = @match.wrestler1
@w2 = @match.wrestler2
@@ -129,10 +178,18 @@ class TournamentsController < ApplicationController
def weigh_in_weight
if params[:wrestler]
Wrestler.update(params[:wrestler].keys, params[:wrestler].values)
sanitized_wrestlers = params.require(:wrestler).to_unsafe_h.each_with_object({}) do |(wrestler_id, attributes), result|
permitted = ActionController::Parameters.new(attributes).permit(:offical_weight)
result[wrestler_id] = permitted
end
Wrestler.update(sanitized_wrestlers.keys, sanitized_wrestlers.values) if sanitized_wrestlers.present?
redirect_to "/tournaments/#{@tournament.id}/weigh_in/#{params[:weight]}", notice: "Weights were successfully recorded."
return
end
if params[:weight]
@weight = Weight.where(:id => params[:weight]).includes(:wrestlers).first
@weight = Weight.where(id: params[:weight])
.includes(wrestlers: [:school, :weight])
.first
@tournament_id = @tournament.id
@tournament_name = @tournament.name
@weights = @tournament.weights
@@ -159,8 +216,11 @@ class TournamentsController < ApplicationController
def all_brackets
@schools = @tournament.schools
@schools = @schools.sort_by{|s| s.page_score_string}.reverse!
@matches = @tournament.matches.includes(:wrestlers,:schools)
@weights = @tournament.weights.includes(:matches,:wrestlers)
@weights = @tournament.weights.includes(:matches, wrestlers: :school)
all_matches = @tournament.matches.includes(:weight, { wrestler1: :school }, { wrestler2: :school })
all_wrestlers = @tournament.wrestlers.includes(:school, :weight)
@matches_by_weight_id = all_matches.group_by(&:weight_id)
@wrestlers_by_weight_id = all_wrestlers.group_by(&:weight_id)
end
def bracket
@@ -196,27 +256,37 @@ class TournamentsController < ApplicationController
end
def qrcode
@tournament_url = tournament_url(@tournament)
@qrcode = RQRCode::QRCode.new(@tournament_url)
end
def up_matches
# .where.not(loser1_name: 'BYE') won't return matches with NULL loser1_name
# so I was only getting back matches with Loser of BOUT_NUMBER
@matches = @tournament.matches
.where("mat_id is NULL and (finished != 1 or finished is NULL)")
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
.order('bout_number ASC')
.limit(10).includes(:wrestlers)
@mats = @tournament.mats.includes(:matches)
@matches = @tournament.up_matches_unassigned_matches
@mats = @tournament.up_matches_mats
end
def bout_sheets
matches_scope = @tournament.matches
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
if params[:round]
round = params[:round]
if round != "All"
@matches = @tournament.matches.where("round = ?",round).sort_by{|match| match.bout_number}
@matches = matches_scope
.where(round: round)
.includes(:weight)
.order(:bout_number)
else
@matches = @tournament.matches.sort_by{|match| match.bout_number}
@matches = matches_scope
.includes(:weight)
.order(:bout_number)
end
wrestler_ids = @matches.flat_map { |match| [match.w1, match.w2] }.compact.uniq
@wrestlers_by_id = Wrestler.includes(:school).where(id: wrestler_ids).index_by(&:id)
end
end

View File

@@ -1,4 +1,20 @@
module ApplicationHelper
def hide_ads?
case controller_name
when "schools"
action_name == "show" && (user_signed_in? || school_permission_key_present?)
when "wrestlers"
%w[new edit].include?(action_name) && (user_signed_in? || school_permission_key_present?)
when "mats"
action_name == "show" && user_signed_in?
else
false
end
end
def school_permission_key_present?
@school_permission_key.present? ||
params[:school_permission_key].present? ||
params.dig(:school, :school_permission_key).present?
end
end

View File

@@ -1,7 +1,9 @@
class AdvanceWrestlerJob < ApplicationJob
queue_as :default
# associations are not available here so we had to pass tournament_id when creating the job
limits_concurrency to: 1, key: ->(_wrestler, _match, tournament_id) { "tournament:#{tournament_id}" }
def perform(wrestler, match)
def perform(wrestler, match, tournament_id)
# Get tournament from wrestler
tournament = wrestler.tournament
@@ -29,4 +31,4 @@ class AdvanceWrestlerJob < ApplicationJob
raise e
end
end
end
end

View File

@@ -1,5 +1,6 @@
class CalculateSchoolScoreJob < ApplicationJob
queue_as :default
limits_concurrency to: 1, key: ->(school) { "tournament:#{school.tournament_id}" }
# Need for TournamentJobStatusIntegrationTest
def self.perform_sync(school)
@@ -35,4 +36,4 @@ class CalculateSchoolScoreJob < ApplicationJob
raise e
end
end
end
end

View File

@@ -1,5 +1,6 @@
class GenerateTournamentMatchesJob < ApplicationJob
queue_as :default
limits_concurrency to: 1, key: ->(tournament) { "tournament:#{tournament.id}" }
def perform(tournament)
# Log information about the job
@@ -17,4 +18,4 @@ class GenerateTournamentMatchesJob < ApplicationJob
raise # Re-raise the error so it's properly recorded
end
end
end
end

View File

@@ -1,5 +1,6 @@
class TournamentBackupJob < ApplicationJob
queue_as :default
limits_concurrency to: 1, key: ->(tournament, *) { "tournament:#{tournament.id}" }
def perform(tournament, reason = nil)
# Log information about the job
@@ -29,4 +30,4 @@ class TournamentBackupJob < ApplicationJob
raise e
end
end
end
end

View File

@@ -17,7 +17,8 @@ class TournamentCleanupJob < ApplicationJob
has_real_matches = tournament.matches.where(finished: 1).where.not(win_type: 'BYE').exists?
if has_real_matches
tournament.tournament_backups.destroy_all
# 1. Remove all school delegates
tournament.schools.each do |school|
school.delegates.destroy_all
@@ -33,4 +34,4 @@ class TournamentCleanupJob < ApplicationJob
end
end
end
end
end

View File

@@ -1,5 +1,6 @@
class WrestlingdevImportJob < ApplicationJob
queue_as :default
limits_concurrency to: 1, key: ->(tournament, *) { "tournament:#{tournament.id}" }
def perform(tournament, import_data = nil)
# Log information about the job
@@ -30,4 +31,4 @@ class WrestlingdevImportJob < ApplicationJob
raise e
end
end
end
end

View File

@@ -1,53 +1,53 @@
class Mat < ApplicationRecord
include ActionView::RecordIdentifier
belongs_to :tournament
has_many :matches, dependent: :nullify
has_many :mat_assignment_rules, dependent: :destroy
validates :name, presence: true
before_destroy do
if tournament.matches.size > 0
tournament.reset_mats
matsToAssign = tournament.mats.select{|m| m.id != self.id}
tournament.assign_mats(matsToAssign)
end
end
QUEUE_SLOTS = %w[queue1 queue2 queue3 queue4].freeze
after_create do
if tournament.matches.size > 0
tournament.reset_mats
matsToAssign = tournament.mats
tournament.assign_mats(matsToAssign)
end
end
after_save :clear_queue_matches_cache
after_commit :broadcast_up_matches_board, on: :update, if: :up_matches_queue_changed?
def assign_next_match
slot = first_empty_queue_slot
return true unless slot
match = next_eligible_match
self.matches.reload
if match and self.unfinished_matches.size < 4
match.mat_id = self.id
if match.save
# Invalidate any wrestler caches
if match.w1
match.wrestler1.touch
match.wrestler1.school.touch
return false unless match
place_match_in_empty_slot!(match, slot)
true
end
def advance_queue!(finished_match = nil)
self.class.transaction do
if finished_match
position = queue_position_for_match(finished_match)
if position == 1
shift_queue_forward!
fill_queue_slots!
elsif position
remove_match_from_queue_and_collapse!(finished_match.id)
else
fill_queue_slots!
end
if match.w2
match.wrestler2.touch
match.wrestler2.school.touch
end
return true
else
return false
if queue1_match&.finished == 1
shift_queue_forward!
end
fill_queue_slots!
end
else
return true
end
broadcast_current_match
true
end
def next_eligible_match
# Start with all matches that are either unfinished (nil or 0), have a bout number, and are ordered by bout_number
filtered_matches = tournament.matches
filtered_matches = Match.where(tournament_id: tournament_id)
.where(finished: [nil, 0]) # finished is nil or 0
.where(mat_id: nil) # mat_id is nil
.where.not(bout_number: nil) # bout_number is not nil
@@ -57,6 +57,11 @@ class Mat < ApplicationRecord
filtered_matches = filtered_matches
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
# Filter out matches without a wrestlers
filtered_matches = filtered_matches
.where("w1 IS NOT NULL")
.where("w2 IS NOT NULL")
# Apply mat assignment rules
mat_assignment_rules.each do |rule|
@@ -80,9 +85,205 @@ class Mat < ApplicationRecord
filtered_matches.first
end
def queue_match_ids
QUEUE_SLOTS.map { |slot| public_send(slot) }
end
# used to prevent N+1 query on each mat
def queue_matches
slot_ids = queue_match_ids
if @queue_matches.nil? || @queue_match_slot_ids != slot_ids
ids = slot_ids.compact
@queue_matches = if ids.empty?
[nil, nil, nil, nil]
else
matches_by_id = Match.where(id: ids)
.includes({ wrestler1: :school }, { wrestler2: :school }, { weight: :matches })
.index_by(&:id)
slot_ids.map { |match_id| match_id ? matches_by_id[match_id] : nil }
end
@queue_match_slot_ids = slot_ids
end
@queue_matches
end
def queue1_match
queue_match_at(1)
end
def queue2_match
queue_match_at(2)
end
def queue3_match
queue_match_at(3)
end
def queue4_match
queue_match_at(4)
end
def queue_position_for_match(match)
return nil unless match
return 1 if queue1 == match.id
return 2 if queue2 == match.id
return 3 if queue3 == match.id
return 4 if queue4 == match.id
nil
end
def remove_match_from_queue_and_collapse!(match_id)
queue_ids = queue_match_ids
return if queue_ids.none? { |id| id == match_id }
queue_ids.map! { |id| id == match_id ? nil : id }
queue_ids = queue_ids.compact
queue_ids += [nil] * (4 - queue_ids.size)
update!(
queue1: queue_ids[0],
queue2: queue_ids[1],
queue3: queue_ids[2],
queue4: queue_ids[3]
)
fill_queue_slots!
broadcast_current_match
end
def assign_match_to_queue!(match, position)
position = position.to_i
raise ArgumentError, "Queue position must be 1-4" unless (1..4).cover?(position)
self.class.transaction do
match.update!(mat_id: id)
remove_match_from_other_mats!(match.id)
queue_ids = queue_match_ids.map { |id| id == match.id ? nil : id }
queue_ids = queue_ids.compact
queue_ids.insert(position - 1, match.id)
bumped_match_id = queue_ids.length > 4 ? queue_ids.pop : nil
queue_ids += [nil] * (4 - queue_ids.length)
update!(
queue1: queue_ids[0],
queue2: queue_ids[1],
queue3: queue_ids[2],
queue4: queue_ids[3]
)
bumped_match = Match.find_by(id: bumped_match_id)
if bumped_match && bumped_match.finished != 1
bumped_match.update!(mat_id: nil)
end
end
broadcast_current_match
end
def clear_queue!
update!(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
broadcast_current_match
end
def unfinished_matches
matches.select{|m| m.finished != 1}.sort_by{|m| m.bout_number}
end
private
def clear_queue_matches_cache
@queue_matches = nil
@queue_match_slot_ids = nil
end
def queue_match_at(position)
queue_matches[position - 1]
end
def first_empty_queue_slot
QUEUE_SLOTS.each_with_index do |slot, index|
return index + 1 if public_send(slot).nil?
end
nil
end
def shift_queue_forward!
update!(
queue1: queue2,
queue2: queue3,
queue3: queue4,
queue4: nil
)
end
def fill_queue_slots!
queue_ids = queue_match_ids
updated = false
QUEUE_SLOTS.each_with_index do |_slot, index|
next if queue_ids[index].present?
match = next_eligible_match
break unless match
queue_ids[index] = match.id
match.update!(mat_id: id)
updated = true
end
if updated
update!(
queue1: queue_ids[0],
queue2: queue_ids[1],
queue3: queue_ids[2],
queue4: queue_ids[3]
)
end
end
def remove_match_from_other_mats!(match_id)
self.class.where.not(id: id)
.where("queue1 = :match_id OR queue2 = :match_id OR queue3 = :match_id OR queue4 = :match_id", match_id: match_id)
.find_each do |mat|
mat.remove_match_from_queue_and_collapse!(match_id)
end
end
def place_match_in_empty_slot!(match, slot)
self.class.transaction do
match.update!(mat_id: id)
remove_match_from_other_mats!(match.id)
update!(slot_key(slot) => match.id)
end
broadcast_current_match
end
def slot_key(slot)
"queue#{slot}"
end
def broadcast_current_match
Turbo::StreamsChannel.broadcast_update_to(
self,
target: dom_id(self, :current_match),
partial: "mats/current_match",
locals: {
mat: self,
match: queue1_match,
next_match: queue2_match,
show_next_bout_button: true
}
)
end
def broadcast_up_matches_board
Tournament.broadcast_up_matches_board(tournament_id)
end
def up_matches_queue_changed?
saved_change_to_queue1? || saved_change_to_queue2? || saved_change_to_queue3? || saved_change_to_queue4?
end
end

View File

@@ -5,6 +5,8 @@ class Match < ApplicationRecord
belongs_to :weight, touch: true
belongs_to :mat, touch: true, optional: true
belongs_to :winner, class_name: 'Wrestler', foreign_key: 'winner_id', optional: true
belongs_to :wrestler1, class_name: 'Wrestler', foreign_key: 'w1', optional: true
belongs_to :wrestler2, class_name: 'Wrestler', foreign_key: 'w2', optional: true
has_many :wrestlers, :through => :weight
has_many :schools, :through => :wrestlers
validate :score_validation, :win_type_validation, :bracket_position_validation, :overtime_type_validation
@@ -15,6 +17,7 @@ class Match < ApplicationRecord
# update mat show with correct match if bout board is reset
# this is done with a turbo stream
after_commit :broadcast_mat_assignment_change, if: :saved_change_to_mat_id?, on: [:create, :update]
after_commit :broadcast_up_matches_board, on: :update, if: :saved_change_to_mat_id?
# Enqueue advancement and related actions after the DB transaction has committed.
# Using after_commit ensures any background jobs enqueued inside these callbacks
@@ -37,12 +40,14 @@ class Match < ApplicationRecord
wrestler2.touch
end
if self.finished == 1 && self.winner_id != nil
if self.mat
self.mat.assign_next_match
end
advance_wrestlers
if self.mat
self.mat.advance_queue!(self)
end
self.tournament.refill_open_bout_board_queues
# School point calculation has move to the end of advance wrestler
# calculate_school_points
self.update(mat_id: nil)
end
end
@@ -176,14 +181,6 @@ class Match < ApplicationRecord
end
end
def wrestler1
wrestlers.select{|w| w.id == self.w1}.first
end
def wrestler2
wrestlers.select{|w| w.id == self.w2}.first
end
def w1_name
if self.w1 != nil
wrestler1.name
@@ -201,6 +198,7 @@ class Match < ApplicationRecord
end
def w1_bracket_name
first_round = first_round_for_weight
return_string = ""
return_string_ending = ""
if self.w1 and self.winner_id == self.w1
@@ -208,7 +206,7 @@ class Match < ApplicationRecord
return_string_ending = return_string_ending + "</strong>"
end
if self.w1 != nil
if self.round == 1
if self.round == first_round
return_string = return_string + "#{wrestler1.long_bracket_name}"
else
return_string = return_string + "#{wrestler1.short_bracket_name}"
@@ -220,6 +218,7 @@ class Match < ApplicationRecord
end
def w2_bracket_name
first_round = first_round_for_weight
return_string = ""
return_string_ending = ""
if self.w2 and self.winner_id == self.w2
@@ -227,7 +226,7 @@ class Match < ApplicationRecord
return_string_ending = return_string_ending + "</strong>"
end
if self.w2 != nil
if self.round == 1
if self.round == first_round
return_string = return_string + "#{wrestler2.long_bracket_name}"
else
return_string = return_string + "#{wrestler2.short_bracket_name}"
@@ -285,6 +284,17 @@ class Match < ApplicationRecord
self.weight.max
end
def first_round_for_weight
return @first_round_for_weight if defined?(@first_round_for_weight)
@first_round_for_weight =
if association(:weight).loaded? && self.weight&.association(:matches)&.loaded?
self.weight.matches.map(&:round).compact.min
else
Match.where(weight_id: self.weight_id).minimum(:round)
end
end
def replace_loser_name_with_wrestler(w,loser_name)
if self.loser1_name == loser_name
self.w1 = w.id
@@ -350,16 +360,20 @@ class Match < ApplicationRecord
next unless mat
Turbo::StreamsChannel.broadcast_update_to(
mat,
target: dom_id(mat, :current_match),
partial: "mats/current_match",
locals: {
mat: mat,
match: mat.unfinished_matches.first,
next_match: mat.unfinished_matches.second,
mat,
target: dom_id(mat, :current_match),
partial: "mats/current_match",
locals: {
mat: mat,
match: mat.queue1_match,
next_match: mat.queue2_match,
show_next_bout_button: true
}
)
end
end
def broadcast_up_matches_board
Tournament.broadcast_up_matches_board(tournament_id)
end
end

View File

@@ -69,8 +69,35 @@ class Tournament < ApplicationRecord
end
end
def up_matches_unassigned_matches
matches
.where("mat_id is NULL and (finished != 1 or finished is NULL)")
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
.order("bout_number ASC")
.limit(10)
.includes({ wrestler1: :school }, { wrestler2: :school }, { weight: :matches })
end
def up_matches_mats
mats.includes(:matches)
end
def self.broadcast_up_matches_board(tournament_id)
tournament = find_by(id: tournament_id)
return unless tournament
Turbo::StreamsChannel.broadcast_replace_to(
tournament,
target: "up_matches_board",
partial: "tournaments/up_matches_board",
locals: { tournament: tournament }
)
end
def destroy_all_matches
matches.destroy_all
mats.each(&:clear_queue!)
end
def matches_by_round(round)
@@ -82,38 +109,26 @@ class Tournament < ApplicationRecord
matches.maximum(:round) || 0 # Return 0 if no matches or max round is nil
end
def assign_mats(mats_to_assign)
if mats_to_assign.count > 0
until mats_to_assign.sort_by{|m| m.id}.last.matches.count == 4
mats_to_assign.sort_by{|m| m.id}.each do |m|
m.assign_next_match
end
end
end
end
def reset_mats
matches.reload
mats.reload
matches_to_reset = matches.select{|m| m.mat_id != nil}
# matches_to_reset.update_all( {:mat_id => nil } )
matches_to_reset.each do |m|
m.mat_id = nil
m.save
end
mats.each do |mat|
mat.clear_queue!
end
end
def pointAdjustments
point_adjustments = []
self.schools.each do |s|
s.deductedPoints.each do |d|
point_adjustments << d
end
end
self.wrestlers.each do |w|
w.deductedPoints.each do |d|
point_adjustments << d
end
end
point_adjustments
school_scope = Teampointadjust.where(school_id: schools.select(:id))
wrestler_scope = Teampointadjust.where(wrestler_id: wrestlers.select(:id))
Teampointadjust.includes(:school, :wrestler)
.merge(school_scope.or(wrestler_scope))
end
def remove_school_delegations
@@ -156,14 +171,14 @@ class Tournament < ApplicationRecord
def double_elim_number_of_wrestlers_error
error_string = ""
if self.tournament_type == "Double Elimination 1-6" or self.tournament_type == "Double Elimination 1-8"
if self.tournament_type == "Regular Double Elimination 1-6" or self.tournament_type == "Regular Double Elimination 1-8"
weights_with_too_many_wrestlers = weights.select{|w| w.wrestlers.size > 64}
weight_with_too_few_wrestlers = weights.select{|w| w.wrestlers.size < 4}
weight_with_too_few_wrestlers = weights.select{|w| w.wrestlers.size < 2}
weights_with_too_many_wrestlers.each do |weight|
error_string = error_string + " The weight class #{weight.max} has more than 64 wrestlers."
end
weight_with_too_few_wrestlers.each do |weight|
error_string = error_string + " The weight class #{weight.max} has less than 4 wrestlers."
error_string = error_string + " The weight class #{weight.max} has less than 2 wrestlers."
end
end
return error_string
@@ -228,19 +243,24 @@ class Tournament < ApplicationRecord
def reset_and_fill_bout_board
reset_mats
if mats.any?
4.times do
# Iterate over each mat and assign the next available match
mats.each do |mat|
match_assigned = mat.assign_next_match
# If no more matches are available, exit early
unless match_assigned
puts "No more eligible matches to assign."
return
end
matches.reload
refill_open_bout_board_queues
end
def refill_open_bout_board_queues
return unless mats.any?
loop do
assigned_any = false
# Fill in round-robin order by queue depth:
# all mats queue1 first, then queue2, then queue3, then queue4.
(1..4).each do |slot|
mats.reload.each do |mat|
next unless mat.public_send("queue#{slot}").nil?
assigned_any ||= mat.assign_next_match
end
end
end
break unless assigned_any
end
end
@@ -279,4 +299,4 @@ class Tournament < ApplicationRecord
def connection_adapter
ActiveRecord::Base.connection.adapter_name
end
end
end

View File

@@ -53,19 +53,16 @@ class User < ApplicationRecord
end
def delegated_tournaments
tournaments_delegated = []
delegated_tournament_permissions.each do |t|
tournaments_delegated << t.tournament
end
tournaments_delegated
Tournament.joins(:delegates)
.where(tournament_delegates: { user_id: id })
.distinct
end
def delegated_schools
schools_delegated = []
delegated_school_permissions.each do |t|
schools_delegated << t.school
end
schools_delegated
School.joins(:delegates)
.where(school_delegates: { user_id: id })
.includes(:tournament)
.distinct
end
def self.search(search)

View File

@@ -156,7 +156,7 @@ class Weight < ApplicationRecord
end
def calculate_bracket_size
num_wrestlers = wrestlers.reload.size
num_wrestlers = wrestlers.size
return nil if num_wrestlers <= 0 # Handle invalid input
# Find the smallest power of 2 greater than or equal to num_wrestlers

View File

@@ -8,25 +8,101 @@ class AdvanceWrestler
def advance
# Use perform_later which will execute based on centralized adapter config
# This will be converted to inline execution in test environment by ActiveJob
AdvanceWrestlerJob.perform_later(@wrestler, @last_match)
AdvanceWrestlerJob.perform_later(@wrestler, @last_match, @tournament.id)
end
def advance_raw
@last_match.reload
@wrestler.reload
if @last_match && @last_match.finished?
pool_to_bracket_advancement if @tournament.tournament_type == "Pool to bracket"
ModifiedDoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
DoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type.include? "Regular Double Elimination"
@last_match = Match.find_by(id: @last_match&.id)
@wrestler = Wrestler.includes(:school, :weight).find_by(id: @wrestler.id)
return unless @last_match && @wrestler && @last_match.finished?
context = preload_advancement_context
matches_to_advance = []
if @tournament.tournament_type == "Pool to bracket"
matches_to_advance.concat(pool_to_bracket_advancement(context))
elsif @tournament.tournament_type.include?("Modified 16 Man Double Elimination")
service = ModifiedDoubleEliminationAdvance.new(@wrestler, @last_match, matches: context[:matches])
service.bracket_advancement
matches_to_advance.concat(service.matches_to_advance)
elsif @tournament.tournament_type.include?("Regular Double Elimination")
service = DoubleEliminationAdvance.new(@wrestler, @last_match, matches: context[:matches])
service.bracket_advancement
matches_to_advance.concat(service.matches_to_advance)
end
persist_advancement_changes(context)
advance_pending_matches(matches_to_advance)
@wrestler.school.calculate_score
end
def pool_to_bracket_advancement
if @wrestler.weight.all_pool_matches_finished(@wrestler.pool) and (@wrestler.finished_bracket_matches.size < 1)
PoolOrder.new(@wrestler.weight.wrestlers_in_pool(@wrestler.pool)).getPoolOrder
end
PoolAdvance.new(@wrestler).advanceWrestler
def preload_advancement_context
weight = Weight.includes(:matches, :wrestlers).find(@wrestler.weight_id)
{
weight: weight,
matches: weight.matches.to_a,
wrestlers: weight.wrestlers.to_a
}
end
end
def persist_advancement_changes(context)
persist_matches(context[:matches])
persist_wrestlers(context[:wrestlers])
end
def persist_matches(matches)
timestamp = Time.current
updates = matches.filter_map do |m|
next unless m.changed?
{
id: m.id,
w1: m.w1,
w2: m.w2,
winner_id: m.winner_id,
win_type: m.win_type,
score: m.score,
finished: m.finished,
loser1_name: m.loser1_name,
loser2_name: m.loser2_name,
finished_at: m.finished_at,
updated_at: timestamp
}
end
Match.upsert_all(updates) if updates.any?
end
def persist_wrestlers(wrestlers)
timestamp = Time.current
updates = wrestlers.filter_map do |w|
next unless w.changed?
{
id: w.id,
pool_placement: w.pool_placement,
pool_placement_tiebreaker: w.pool_placement_tiebreaker,
updated_at: timestamp
}
end
Wrestler.upsert_all(updates) if updates.any?
end
def advance_pending_matches(matches_to_advance)
matches_to_advance.uniq(&:id).each do |match|
match.advance_wrestlers
end
end
def pool_to_bracket_advancement(context)
matches_to_advance = []
wrestlers_in_pool = context[:wrestlers].select { |w| w.pool == @wrestler.pool }
if @wrestler.weight.all_pool_matches_finished(@wrestler.pool) && (@wrestler.finished_bracket_matches.size < 1)
PoolOrder.new(wrestlers_in_pool).getPoolOrder
end
service = PoolAdvance.new(@wrestler, @last_match, matches: context[:matches], wrestlers: context[:wrestlers])
service.advanceWrestler
matches_to_advance.concat(service.matches_to_advance)
matches_to_advance
end
end

View File

@@ -1,8 +1,12 @@
class DoubleEliminationAdvance
def initialize(wrestler,last_match)
attr_reader :matches_to_advance
def initialize(wrestler,last_match, matches: nil)
@wrestler = wrestler
@last_match = last_match
@matches = matches || @wrestler.weight.matches.to_a
@matches_to_advance = []
@next_match_position_number = (@last_match.bracket_position_number / 2.0)
end
@@ -48,7 +52,7 @@ class DoubleEliminationAdvance
end
if next_match_bracket_position
next_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?",next_match_bracket_position,next_match_position_number.ceil,@wrestler.weight_id).first
next_match = @matches.find { |m| m.bracket_position == next_match_bracket_position && m.bracket_position_number == next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
end
if next_match
@@ -59,18 +63,16 @@ class DoubleEliminationAdvance
def update_new_match(match, wrestler_number)
if wrestler_number == 2 or (match.loser1_name and match.loser1_name.include? "Loser of")
match.w2 = @wrestler.id
match.save
elsif wrestler_number == 1
match.w1 = @wrestler.id
match.save
end
end
def update_consolation_bye
bout = @wrestler.last_match.bout_number
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?","Loser of #{bout}","Loser of #{bout}",@wrestler.weight_id)
if next_match.size > 0
next_match.first.replace_loser_name_with_bye("Loser of #{bout}")
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
if next_match
replace_loser_name_with_bye(next_match, "Loser of #{bout}")
end
end
@@ -84,27 +86,18 @@ class DoubleEliminationAdvance
def losers_bracket_advancement
bout = @last_match.bout_number
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?", "Loser of #{bout}", "Loser of #{bout}", @wrestler.weight_id).first
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
if next_match.present?
next_match.replace_loser_name_with_wrestler(@wrestler, "Loser of #{bout}")
next_match.reload
replace_loser_name_with_wrestler(next_match, @wrestler, "Loser of #{bout}")
if next_match.loser1_name == "BYE" || next_match.loser2_name == "BYE"
next_match.winner_id = @wrestler.id
next_match.win_type = "BYE"
next_match.score = ""
next_match.finished = 1
# puts "Before save: winner_id=#{next_match.winner_id}"
# if next_match.save
# puts "Save successful: winner_id=#{next_match.reload.winner_id}"
# else
# puts "Save failed: #{next_match.errors.full_messages}"
# end
next_match.save
next_match.advance_wrestlers
next_match.finished_at = Time.current
@matches_to_advance << next_match
end
end
end
@@ -112,51 +105,69 @@ class DoubleEliminationAdvance
def advance_double_byes
weight = @wrestler.weight
weight.matches.select{|m| m.loser1_name == "BYE" and m.loser2_name == "BYE" and m.finished != 1}.each do |match|
@matches.select{|m| m.weight_id == weight.id && m.loser1_name == "BYE" and m.loser2_name == "BYE" and m.finished != 1}.each do |match|
match.finished = 1
match.finished_at = Time.current
match.score = ""
match.win_type = "BYE"
next_match_position_number = (match.bracket_position_number / 2.0).ceil
after_matches = weight.matches.select{|m| m.round > match.round and m.is_consolation_match == match.is_consolation_match }.sort_by{|m| m.round}
next_matches = weight.matches.select{|m| m.round == after_matches.first.round and m.is_consolation_match == match.is_consolation_match }
this_round_matches = weight.matches.select{|m| m.round == match.round and m.is_consolation_match == match.is_consolation_match }
after_matches = @matches.select{|m| m.weight_id == weight.id && m.round > match.round and m.is_consolation_match == match.is_consolation_match }.sort_by{|m| m.round}
next if after_matches.empty?
next_matches = @matches.select{|m| m.weight_id == weight.id && m.round == after_matches.first.round and m.is_consolation_match == match.is_consolation_match }
this_round_matches = @matches.select{|m| m.weight_id == weight.id && m.round == match.round and m.is_consolation_match == match.is_consolation_match }
next_match = nil
if next_matches.size == this_round_matches.size
next_match = next_matches.select{|m| m.bracket_position_number == match.bracket_position_number}.first
next_match.loser2_name = "BYE"
next_match.save
next_match.loser2_name = "BYE" if next_match
elsif next_matches.size < this_round_matches.size and next_matches.size > 0
next_match = next_matches.select{|m| m.bracket_position_number == next_match_position_number}.first
if next_match.bracket_position_number == next_match_position_number
if next_match && next_match.bracket_position_number == next_match_position_number
next_match.loser2_name = "BYE"
else
elsif next_match
next_match.loser1_name = "BYE"
end
end
next_match.save
match.save
end
end
def set_bye_for_placement
weight = @wrestler.weight
fifth_finals = weight.matches.select{|match| match.bracket_position == '5/6'}.first
seventh_finals = weight.matches.select{|match| match.bracket_position == '7/8'}.first
fifth_finals = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == '5/6'}.first
seventh_finals = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == '7/8'}.first
if seventh_finals
conso_quarter = weight.matches.select{|match| match.bracket_position == 'Conso Quarter'}
conso_quarter = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == 'Conso Quarter'}
conso_quarter.each do |match|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
seventh_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
replace_loser_name_with_bye(seventh_finals, "Loser of #{match.bout_number}")
end
end
end
if fifth_finals
conso_semis = weight.matches.select{|match| match.bracket_position == 'Conso Semis'}
conso_semis = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == 'Conso Semis'}
conso_semis.each do |match|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
fifth_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
replace_loser_name_with_bye(fifth_finals, "Loser of #{match.bout_number}")
end
end
end
end
def replace_loser_name_with_wrestler(match, wrestler, loser_name)
if match.loser1_name == loser_name
match.w1 = wrestler.id
end
if match.loser2_name == loser_name
match.w2 = wrestler.id
end
end
def replace_loser_name_with_bye(match, loser_name)
if match.loser1_name == loser_name
match.loser1_name = "BYE"
end
if match.loser2_name == loser_name
match.loser2_name = "BYE"
end
end
end

View File

@@ -1,8 +1,12 @@
class ModifiedDoubleEliminationAdvance
def initialize(wrestler,last_match)
attr_reader :matches_to_advance
def initialize(wrestler,last_match, matches: nil)
@wrestler = wrestler
@last_match = last_match
@matches = matches || @wrestler.weight.matches.to_a
@matches_to_advance = []
@next_match_position_number = (@last_match.bracket_position_number / 2.0)
end
@@ -25,42 +29,41 @@ class ModifiedDoubleEliminationAdvance
update_consolation_bye
end
if @last_match.bracket_position == "Quarter"
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Semis",@next_match_position_number.ceil,@wrestler.weight_id).first
new_match = @matches.find { |m| m.bracket_position == "Semis" && m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
update_new_match(new_match, get_wrestler_number)
elsif @last_match.bracket_position == "Semis"
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","1/2",@next_match_position_number.ceil,@wrestler.weight_id).first
new_match = @matches.find { |m| m.bracket_position == "1/2" && m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
update_new_match(new_match, get_wrestler_number)
elsif @last_match.bracket_position == "Conso Semis"
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","5/6",@next_match_position_number.ceil,@wrestler.weight_id).first
new_match = @matches.find { |m| m.bracket_position == "5/6" && m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
update_new_match(new_match, get_wrestler_number)
elsif @last_match.bracket_position == "Conso Quarter"
# it's a special bracket where a semi loser is not dropping down
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Conso Semis",@next_match_position_number.ceil,@wrestler.weight_id).first
new_match = @matches.find { |m| m.bracket_position == "Conso Semis" && m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
update_new_match(new_match, get_wrestler_number)
elsif @last_match.bracket_position == "Bracket Round of 16"
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and bracket_position = ?", @next_match_position_number.ceil,@wrestler.weight_id, @last_match.round , "Quarter").sort_by{|m| m.round}.first
new_match = @matches.select { |m| m.bracket_position_number == @next_match_position_number.ceil && m.weight_id == @wrestler.weight_id && m.round > @last_match.round && m.bracket_position == "Quarter" }.sort_by(&:round).first
update_new_match(new_match, get_wrestler_number)
elsif @last_match.bracket_position == "Conso Round of 8"
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and bracket_position = ?", @last_match.bracket_position_number,@wrestler.weight_id, @last_match.round, "Conso Quarter").sort_by{|m| m.round}.first
new_match = @matches.select { |m| m.bracket_position_number == @last_match.bracket_position_number && m.weight_id == @wrestler.weight_id && m.round > @last_match.round && m.bracket_position == "Conso Quarter" }.sort_by(&:round).first
update_new_match(new_match, get_wrestler_number)
end
end
def update_new_match(match, wrestler_number)
return unless match
if wrestler_number == 2 or (match.loser1_name and match.loser1_name.include? "Loser of")
match.w2 = @wrestler.id
match.save
elsif wrestler_number == 1
match.w1 = @wrestler.id
match.save
end
end
def update_consolation_bye
bout = @wrestler.last_match.bout_number
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?","Loser of #{bout}","Loser of #{bout}",@wrestler.weight_id)
if next_match.size > 0
next_match.first.replace_loser_name_with_bye("Loser of #{bout}")
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
if next_match
replace_loser_name_with_bye(next_match, "Loser of #{bout}")
end
end
@@ -74,27 +77,18 @@ class ModifiedDoubleEliminationAdvance
def losers_bracket_advancement
bout = @last_match.bout_number
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?", "Loser of #{bout}", "Loser of #{bout}", @wrestler.weight_id).first
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
if next_match.present?
next_match.replace_loser_name_with_wrestler(@wrestler, "Loser of #{bout}")
next_match.reload
replace_loser_name_with_wrestler(next_match, @wrestler, "Loser of #{bout}")
if next_match.loser1_name == "BYE" || next_match.loser2_name == "BYE"
next_match.winner_id = @wrestler.id
next_match.win_type = "BYE"
next_match.score = ""
next_match.finished = 1
# puts "Before save: winner_id=#{next_match.winner_id}"
# if next_match.save
# puts "Save successful: winner_id=#{next_match.reload.winner_id}"
# else
# puts "Save failed: #{next_match.errors.full_messages}"
# end
next_match.save
next_match.advance_wrestlers
next_match.finished_at = Time.current
@matches_to_advance << next_match
end
end
end
@@ -102,43 +96,53 @@ class ModifiedDoubleEliminationAdvance
def advance_double_byes
weight = @wrestler.weight
weight.matches.select{|m| m.loser1_name == "BYE" and m.loser2_name == "BYE" and m.finished != 1}.each do |match|
@matches.select{|m| m.weight_id == weight.id && m.loser1_name == "BYE" and m.loser2_name == "BYE" and m.finished != 1}.each do |match|
match.finished = 1
match.finished_at = Time.current
match.score = ""
match.win_type = "BYE"
next_match_position_number = (match.bracket_position_number / 2.0).ceil
after_matches = weight.matches.select{|m| m.round > match.round and m.is_consolation_match == match.is_consolation_match }.sort_by{|m| m.round}
next_matches = weight.matches.select{|m| m.round == after_matches.first.round and m.is_consolation_match == match.is_consolation_match }
this_round_matches = weight.matches.select{|m| m.round == match.round and m.is_consolation_match == match.is_consolation_match }
after_matches = @matches.select{|m| m.weight_id == weight.id && m.round > match.round and m.is_consolation_match == match.is_consolation_match }.sort_by{|m| m.round}
next if after_matches.empty?
next_matches = @matches.select{|m| m.weight_id == weight.id && m.round == after_matches.first.round and m.is_consolation_match == match.is_consolation_match }
this_round_matches = @matches.select{|m| m.weight_id == weight.id && m.round == match.round and m.is_consolation_match == match.is_consolation_match }
next_match = nil
if next_matches.size == this_round_matches.size
next_match = next_matches.select{|m| m.bracket_position_number == match.bracket_position_number}.first
next_match.loser2_name = "BYE"
next_match.save
next_match.loser2_name = "BYE" if next_match
elsif next_matches.size < this_round_matches.size and next_matches.size > 0
next_match = next_matches.select{|m| m.bracket_position_number == next_match_position_number}.first
if next_match.bracket_position_number == next_match_position_number
if next_match && next_match.bracket_position_number == next_match_position_number
next_match.loser2_name = "BYE"
else
elsif next_match
next_match.loser1_name = "BYE"
end
end
next_match.save
match.save
end
end
def set_bye_for_placement
weight = @wrestler.weight
seventh_finals = weight.matches.select{|match| match.bracket_position == '7/8'}.first
seventh_finals = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == '7/8'}.first
if seventh_finals
conso_quarter = weight.matches.select{|match| match.bracket_position == 'Conso Semis'}
conso_quarter = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == 'Conso Semis'}
conso_quarter.each do |match|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
seventh_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
replace_loser_name_with_bye(seventh_finals, "Loser of #{match.bout_number}")
end
end
end
end
def replace_loser_name_with_wrestler(match, wrestler, loser_name)
match.w1 = wrestler.id if match.loser1_name == loser_name
match.w2 = wrestler.id if match.loser2_name == loser_name
end
def replace_loser_name_with_bye(match, loser_name)
match.loser1_name = "BYE" if match.loser1_name == loser_name
match.loser2_name = "BYE" if match.loser2_name == loser_name
end
end

View File

@@ -1,8 +1,13 @@
class PoolAdvance
def initialize(wrestler)
attr_reader :matches_to_advance
def initialize(wrestler, last_match, matches: nil, wrestlers: nil)
@wrestler = wrestler
@last_match = @wrestler.last_match
@last_match = last_match
@matches = matches || @wrestler.weight.matches.to_a
@wrestlers = wrestlers || @wrestler.weight.wrestlers.to_a
@matches_to_advance = []
end
def advanceWrestler
@@ -17,15 +22,15 @@ class PoolAdvance
def poolToBracketAdvancment
pool = @wrestler.pool
# This has to always run because the last match in a pool might not be a pool winner or runner up
winner = Wrestler.where("weight_id = ? and pool_placement = 1 and pool = ?",@wrestler.weight.id, pool).first
runner_up = Wrestler.where("weight_id = ? and pool_placement = 2 and pool = ?",@wrestler.weight.id, pool).first
winner = @wrestlers.find { |w| w.weight_id == @wrestler.weight.id && w.pool_placement == 1 && w.pool == pool }
runner_up = @wrestlers.find { |w| w.weight_id == @wrestler.weight.id && w.pool_placement == 2 && w.pool == pool }
if runner_up
runner_up_match = Match.where("weight_id = ? and (loser1_name = ? or loser2_name = ?)",@wrestler.weight.id, "Runner Up Pool #{pool}", "Runner Up Pool #{pool}").first
runner_up_match.replace_loser_name_with_wrestler(runner_up,"Runner Up Pool #{pool}")
runner_up_match = @matches.find { |m| m.weight_id == @wrestler.weight.id && (m.loser1_name == "Runner Up Pool #{pool}" || m.loser2_name == "Runner Up Pool #{pool}") }
replace_loser_name_with_wrestler(runner_up_match, runner_up, "Runner Up Pool #{pool}") if runner_up_match
end
if winner
winner_match = Match.where("weight_id = ? and (loser1_name = ? or loser2_name = ?)",@wrestler.weight.id, "Winner Pool #{pool}", "Winner Pool #{pool}").first
winner_match.replace_loser_name_with_wrestler(winner,"Winner Pool #{pool}")
winner_match = @matches.find { |m| m.weight_id == @wrestler.weight.id && (m.loser1_name == "Winner Pool #{pool}" || m.loser2_name == "Winner Pool #{pool}") }
replace_loser_name_with_wrestler(winner_match, winner, "Winner Pool #{pool}") if winner_match
end
end
@@ -45,36 +50,40 @@ class PoolAdvance
def winner_advance
if @wrestler.last_match.bracket_position == "Quarter"
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Semis",@wrestler.next_match_position_number.ceil,@wrestler.weight_id).first
new_match = @matches.find { |m| m.bracket_position == "Semis" && m.bracket_position_number == @wrestler.next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
updateNewMatch(new_match)
end
if @wrestler.last_match.bracket_position == "Semis"
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","1/2",@wrestler.next_match_position_number.ceil,@wrestler.weight_id).first
new_match = @matches.find { |m| m.bracket_position == "1/2" && m.bracket_position_number == @wrestler.next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
updateNewMatch(new_match)
end
if @wrestler.last_match.bracket_position == "Conso Semis"
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","5/6",@wrestler.next_match_position_number.ceil,@wrestler.weight_id).first
new_match = @matches.find { |m| m.bracket_position == "5/6" && m.bracket_position_number == @wrestler.next_match_position_number.ceil && m.weight_id == @wrestler.weight_id }
updateNewMatch(new_match)
end
end
def updateNewMatch(match)
return unless match
if @wrestler.next_match_position_number == @wrestler.next_match_position_number.ceil
match.w2 = @wrestler.id
match.save
end
if @wrestler.next_match_position_number != @wrestler.next_match_position_number.ceil
match.w1 = @wrestler.id
match.save
end
end
def loser_advance
bout = @wrestler.last_match.bout_number
next_match = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?","Loser of #{bout}","Loser of #{bout}",@wrestler.weight_id)
if next_match.size > 0
next_match.first.replace_loser_name_with_wrestler(@wrestler,"Loser of #{bout}")
next_match = @matches.find { |m| m.weight_id == @wrestler.weight_id && (m.loser1_name == "Loser of #{bout}" || m.loser2_name == "Loser of #{bout}") }
if next_match
replace_loser_name_with_wrestler(next_match, @wrestler, "Loser of #{bout}")
end
end
def replace_loser_name_with_wrestler(match, wrestler, loser_name)
match.w1 = wrestler.id if match.loser1_name == loser_name
match.w2 = wrestler.id if match.loser2_name == loser_name
end
end

View File

@@ -4,8 +4,6 @@ class PoolOrder
end
def getPoolOrder
# clear caching for weight for bracket page
@wrestlers.first.weight.touch
setOriginalPoints
while checkForTies(@wrestlers) == true
getWrestlersOrderByPoolAdvancePoints.each do |wrestler|
@@ -18,7 +16,6 @@ class PoolOrder
getWrestlersOrderByPoolAdvancePoints.each_with_index do |wrestler, index|
placement = index + 1
wrestler.pool_placement = placement
wrestler.save
end
@wrestlers.sort_by{|w| w.poolAdvancePoints}.reverse!
end
@@ -29,7 +26,6 @@ class PoolOrder
def setOriginalPoints
@wrestlers.each do |w|
matches = w.reload.all_matches
w.pool_placement_tiebreaker = nil
w.pool_placement = nil
w.poolAdvancePoints = w.pool_wins.size

View File

@@ -0,0 +1,55 @@
class CalculateSchoolScore
def initialize(school)
@school = school
end
def calculate
school = preload_school_context
score = calculate_score_value(school)
persist_school_score(school.id, score)
score
end
def calculate_value
school = preload_school_context
calculate_score_value(school)
end
private
def preload_school_context
School
.includes(
:deductedPoints,
wrestlers: [
:deductedPoints,
{ matches_as_w1: :winner },
{ matches_as_w2: :winner },
{ weight: [:matches, { tournament: { weights: :wrestlers } }] }
]
)
.find(@school.id)
end
def calculate_score_value(school)
total_points_scored_by_wrestlers(school) - total_points_deducted(school)
end
def total_points_scored_by_wrestlers(school)
school.wrestlers.sum { |wrestler| CalculateWrestlerTeamScore.new(wrestler).totalScore }
end
def total_points_deducted(school)
school.deductedPoints.sum(&:points)
end
def persist_school_score(school_id, score)
School.upsert_all([
{
id: school_id,
score: score,
updated_at: Time.current
}
])
end
end

View File

@@ -3,30 +3,30 @@ class DoubleEliminationGenerateLoserNames
@tournament = tournament
end
# Entry point: assign loser placeholders and advance any byes
def assign_loser_names
# Compatibility wrapper. Returns transformed rows and does not persist.
def assign_loser_names(match_rows = nil)
rows = match_rows || @tournament.matches.where(tournament_id: @tournament.id).map { |m| m.attributes.symbolize_keys }
@tournament.weights.each do |weight|
assign_loser_names_for_weight(weight)
advance_bye_matches_championship(weight)
advance_bye_matches_consolation(weight)
next unless weight.calculate_bracket_size > 2
assign_loser_names_in_memory(weight, rows)
assign_bye_outcomes_in_memory(weight, rows)
end
rows
end
private
# Assign loser names for a single weight bracket
def assign_loser_names_for_weight(weight)
def assign_loser_names_in_memory(weight, match_rows)
bracket_size = weight.calculate_bracket_size
matches = weight.matches.reload
num_placers = @tournament.number_of_placers
return if bracket_size <= 2
rows = match_rows.select { |row| row[:weight_id] == weight.id }
num_placers = @tournament.number_of_placers
# Build dynamic round definitions
champ_rounds = dynamic_championship_rounds(bracket_size)
conso_rounds = dynamic_consolation_rounds(bracket_size)
first_round = { bracket_position: first_round_label(bracket_size) }
champ_full = [first_round] + champ_rounds
first_round = { bracket_position: first_round_label(bracket_size) }
champ_full = [first_round] + champ_rounds
# Map championship losers into consolation slots
mappings = []
champ_full[0...-1].each_with_index do |champ_info, i|
map_idx = i.zero? ? 0 : (2 * i - 1)
@@ -34,128 +34,109 @@ class DoubleEliminationGenerateLoserNames
mappings << {
championship_bracket_position: champ_info[:bracket_position],
consolation_bracket_position: conso_rounds[map_idx][:bracket_position],
both_wrestlers: i.zero?,
champ_round_index: i
consolation_bracket_position: conso_rounds[map_idx][:bracket_position],
both_wrestlers: i.zero?,
champ_round_index: i
}
end
# Apply loser-name mappings
mappings.each do |map|
champ = matches.select { |m| m.bracket_position == map[:championship_bracket_position] }
.sort_by(&:bracket_position_number)
conso = matches.select { |m| m.bracket_position == map[:consolation_bracket_position] }
.sort_by(&:bracket_position_number)
current_champ_round_index = map[:champ_round_index]
if current_champ_round_index.odd?
conso.reverse!
end
champ = rows.select { |r| r[:bracket_position] == map[:championship_bracket_position] }
.sort_by { |r| r[:bracket_position_number] }
conso = rows.select { |r| r[:bracket_position] == map[:consolation_bracket_position] }
.sort_by { |r| r[:bracket_position_number] }
conso.reverse! if map[:champ_round_index].odd?
idx = 0
# Determine if this mapping is for losers from the first championship round
is_first_champ_round_feed = map[:champ_round_index].zero?
is_first_feed = map[:champ_round_index].zero?
conso.each do |cm|
champ_match1 = champ[idx]
if champ_match1
if is_first_champ_round_feed && ((champ_match1.w1 && champ_match1.w2.nil?) || (champ_match1.w1.nil? && champ_match1.w2))
cm.loser1_name = "BYE"
if is_first_feed && single_competitor_match_row?(champ_match1)
cm[:loser1_name] = "BYE"
else
cm.loser1_name = "Loser of #{champ_match1.bout_number}"
cm[:loser1_name] = "Loser of #{champ_match1[:bout_number]}"
end
else
cm.loser1_name = nil # Should not happen if bracket generation is correct
cm[:loser1_name] = nil
end
if map[:both_wrestlers] # This is true only if is_first_champ_round_feed
idx += 1 # Increment for the second championship match
if map[:both_wrestlers]
idx += 1
champ_match2 = champ[idx]
if champ_match2
# BYE check is only relevant for the first championship round feed
if is_first_champ_round_feed && ((champ_match2.w1 && champ_match2.w2.nil?) || (champ_match2.w1.nil? && champ_match2.w2))
cm.loser2_name = "BYE"
if is_first_feed && single_competitor_match_row?(champ_match2)
cm[:loser2_name] = "BYE"
else
cm.loser2_name = "Loser of #{champ_match2.bout_number}"
cm[:loser2_name] = "Loser of #{champ_match2[:bout_number]}"
end
else
cm.loser2_name = nil # Should not happen
cm[:loser2_name] = nil
end
end
idx += 1 # Increment for the next consolation match or next pair from championship
idx += 1
end
end
# 5th/6th place
if bracket_size >= 5 && num_placers >= 6
conso_semis = matches.select { |m| m.bracket_position == "Conso Semis" }
.sort_by(&:bracket_position_number)
if conso_semis.size >= 2
m56 = matches.find { |m| m.bracket_position == "5/6" }
m56.loser1_name = "Loser of #{conso_semis[0].bout_number}"
m56.loser2_name = "Loser of #{conso_semis[1].bout_number}" if m56
if bracket_size >= 5 && num_placers >= 6 && weight.wrestlers.size > 4
conso_semis = rows.select { |r| r[:bracket_position] == "Conso Semis" }.sort_by { |r| r[:bracket_position_number] }
m56 = rows.find { |r| r[:bracket_position] == "5/6" }
if conso_semis.size >= 2 && m56
m56[:loser1_name] = "Loser of #{conso_semis[0][:bout_number]}"
m56[:loser2_name] = "Loser of #{conso_semis[1][:bout_number]}"
end
end
# 7th/8th place
if bracket_size >= 7 && num_placers >= 8
conso_quarters = matches.select { |m| m.bracket_position == "Conso Quarter" }
.sort_by(&:bracket_position_number)
if conso_quarters.size >= 2
m78 = matches.find { |m| m.bracket_position == "7/8" }
m78.loser1_name = "Loser of #{conso_quarters[0].bout_number}"
m78.loser2_name = "Loser of #{conso_quarters[1].bout_number}" if m78
if bracket_size >= 7 && num_placers >= 8 && weight.wrestlers.size > 6
conso_quarters = rows.select { |r| r[:bracket_position] == "Conso Quarter" }.sort_by { |r| r[:bracket_position_number] }
m78 = rows.find { |r| r[:bracket_position] == "7/8" }
if conso_quarters.size >= 2 && m78
m78[:loser1_name] = "Loser of #{conso_quarters[0][:bout_number]}"
m78[:loser2_name] = "Loser of #{conso_quarters[1][:bout_number]}"
end
end
matches.each(&:save!)
end
# Advance first-round byes in championship bracket
def advance_bye_matches_championship(weight)
matches = weight.matches.reload
first_round = matches.map(&:round).min
matches.select { |m| m.round == first_round }
.sort_by(&:bracket_position_number)
.each { |m| handle_bye(m) }
end
# Advance first-round byes in consolation bracket
def advance_bye_matches_consolation(weight)
matches = weight.matches.reload
def assign_bye_outcomes_in_memory(weight, match_rows)
bracket_size = weight.calculate_bracket_size
first_conso = dynamic_consolation_rounds(bracket_size).first
return if bracket_size <= 2
matches.select { |m| m.round == first_conso[:round] && m.bracket_position == first_conso[:bracket_position] }
.sort_by(&:bracket_position_number)
.each { |m| handle_bye(m) }
end
rows = match_rows.select { |r| r[:weight_id] == weight.id }
first_round = rows.map { |r| r[:round] }.compact.min
rows.select { |r| r[:round] == first_round }.each { |row| apply_bye_to_row(row) }
# Mark bye match, set finished, and advance
def handle_bye(match)
if [match.w1, match.w2].compact.size == 1
match.finished = 1
match.win_type = 'BYE'
if match.w1
match.winner_id = match.w1
match.loser2_name = 'BYE'
else
match.winner_id = match.w2
match.loser1_name = 'BYE'
end
match.score = ''
match.save!
match.advance_wrestlers
first_conso = dynamic_consolation_rounds(bracket_size).first
if first_conso
rows.select { |r| r[:round] == first_conso[:round] && r[:bracket_position] == first_conso[:bracket_position] }
.each { |row| apply_bye_to_row(row) }
end
end
# Helpers for dynamic bracket labels
def apply_bye_to_row(row)
return unless single_competitor_match_row?(row)
row[:finished] = 1
row[:win_type] = "BYE"
if row[:w1]
row[:winner_id] = row[:w1]
row[:loser2_name] = "BYE"
else
row[:winner_id] = row[:w2]
row[:loser1_name] = "BYE"
end
row[:score] = ""
end
def single_competitor_match_row?(row)
[row[:w1], row[:w2]].compact.size == 1
end
def first_round_label(size)
case size
when 2 then 'Final'
when 4 then 'Semis'
when 8 then 'Quarter'
else "Bracket Round of #{size}"
when 2 then "Final"
when 4 then "Semis"
when 8 then "Quarter"
else "Bracket Round of #{size}"
end
end
@@ -170,36 +151,36 @@ class DoubleEliminationGenerateLoserNames
def dynamic_consolation_rounds(size)
total_log2 = Math.log2(size).to_i
return [] if total_log2 <= 1
max_j_val = (2 * (total_log2 - 1) - 1)
(1..max_j_val).map do |j|
current_participants = size / (2**((j.to_f / 2).ceil))
{
{
bracket_position: consolation_label(current_participants, j, size),
round: j
round: j
}
end
end
def bracket_label(participants)
case participants
when 2 then '1/2'
when 4 then 'Semis'
when 8 then 'Quarter'
else "Bracket Round of #{participants}"
when 2 then "1/2"
when 4 then "Semis"
when 8 then "Quarter"
else "Bracket Round of #{participants}"
end
end
def consolation_label(participants, j, bracket_size)
max_j_for_bracket = (2 * (Math.log2(bracket_size).to_i - 1) - 1)
if participants == 2 && j == max_j_for_bracket
return '3/4'
"3/4"
elsif participants == 4
return j.odd? ? 'Conso Quarter' : 'Conso Semis'
j.odd? ? "Conso Quarter" : "Conso Semis"
else
suffix = j.odd? ? ".1" : ".2"
return "Conso Round of #{participants}#{suffix}"
"Conso Round of #{participants}#{suffix}"
end
end
end

View File

@@ -1,29 +1,33 @@
class DoubleEliminationMatchGeneration
def initialize(tournament)
def initialize(tournament, weights: nil)
@tournament = tournament
@weights = weights
end
def generate_matches
#
# PHASE 1: Generate matches (with local round definitions).
#
@tournament.weights.each do |weight|
generate_matches_for_weight(weight)
build_match_rows
end
def build_match_rows
rows_by_weight_id = {}
generation_weights.each do |weight|
rows_by_weight_id[weight.id] = generate_match_rows_for_weight(weight)
end
#
# PHASE 2: Align all rounds to match the largest brackets definitions.
#
align_all_rounds_to_largest_bracket
align_rows_to_largest_bracket(rows_by_weight_id)
rows_by_weight_id.values.flatten
end
###########################################################################
# PHASE 1: Generate all matches for each bracket, using a single definition.
###########################################################################
def generate_matches_for_weight(weight)
def generate_match_rows_for_weight(weight)
bracket_size = weight.calculate_bracket_size
bracket_info = define_bracket_matches(bracket_size)
return unless bracket_info
return [] unless bracket_info
rows = []
# 1) Round one matchups
bracket_info[:round_one_matchups].each_with_index do |matchup, idx|
@@ -32,7 +36,7 @@ class DoubleEliminationMatchGeneration
bracket_pos_number = idx + 1
round_number = matchup[:round]
create_matchup_from_seed(
rows << create_matchup_from_seed(
seed1,
seed2,
bracket_position,
@@ -49,7 +53,7 @@ class DoubleEliminationMatchGeneration
round_number = round_info[:round]
matches_this_round.times do |i|
create_matchup(
rows << create_matchup(
nil,
nil,
bracket_position,
@@ -67,7 +71,7 @@ class DoubleEliminationMatchGeneration
round_number = round_info[:round]
matches_this_round.times do |i|
create_matchup(
rows << create_matchup(
nil,
nil,
bracket_position,
@@ -79,12 +83,14 @@ class DoubleEliminationMatchGeneration
# 5/6, 7/8 placing logic
if weight.wrestlers.size >= 5 && @tournament.number_of_placers >= 6 && matches_this_round == 1
create_matchup(nil, nil, "5/6", 1, round_number, weight)
rows << create_matchup(nil, nil, "5/6", 1, round_number, weight)
end
if weight.wrestlers.size >= 7 && @tournament.number_of_placers >= 8 && matches_this_round == 1
create_matchup(nil, nil, "7/8", 1, round_number, weight)
rows << create_matchup(nil, nil, "7/8", 1, round_number, weight)
end
end
rows
end
# Single bracket definition dynamically generated for any power-of-two bracket size.
@@ -173,18 +179,18 @@ class DoubleEliminationMatchGeneration
###########################################################################
# PHASE 2: Overwrite rounds in all smaller brackets to match the largest one.
###########################################################################
def align_all_rounds_to_largest_bracket
largest_weight = @tournament.weights.max_by { |w| w.calculate_bracket_size }
def align_rows_to_largest_bracket(rows_by_weight_id)
largest_weight = generation_weights.max_by { |w| w.calculate_bracket_size }
return unless largest_weight
position_to_round = {}
largest_weight.tournament.matches.where(weight_id: largest_weight.id).each do |m|
position_to_round[m.bracket_position] ||= m.round
rows_by_weight_id.fetch(largest_weight.id, []).each do |row|
position_to_round[row[:bracket_position]] ||= row[:round]
end
@tournament.matches.find_each do |match|
if position_to_round.key?(match.bracket_position)
match.update(round: position_to_round[match.bracket_position])
rows_by_weight_id.each_value do |rows|
rows.each do |row|
row[:round] = position_to_round[row[:bracket_position]] if position_to_round.key?(row[:bracket_position])
end
end
end
@@ -192,8 +198,12 @@ class DoubleEliminationMatchGeneration
###########################################################################
# Helper methods
###########################################################################
def generation_weights
@weights || @tournament.weights.to_a
end
def wrestler_with_seed(seed, weight)
Wrestler.where("weight_id = ? AND bracket_line = ?", weight.id, seed).first&.id
weight.wrestlers.find { |w| w.bracket_line == seed }&.id
end
def create_matchup_from_seed(w1_seed, w2_seed, bracket_position, bracket_position_number, round, weight)
@@ -208,14 +218,15 @@ class DoubleEliminationMatchGeneration
end
def create_matchup(w1, w2, bracket_position, bracket_position_number, round, weight)
weight.tournament.matches.create!(
{
w1: w1,
w2: w2,
tournament_id: weight.tournament_id,
weight_id: weight.id,
round: round,
bracket_position: bracket_position,
bracket_position_number: bracket_position_number
)
}
end
# Calculates the sequence of seeds for the first round of a power-of-two bracket.

View File

@@ -10,62 +10,183 @@ class GenerateTournamentMatches
def generate_raw
standardStartingActions
PoolToBracketMatchGeneration.new(@tournament).generatePoolToBracketMatches if @tournament.tournament_type == "Pool to bracket"
ModifiedSixteenManMatchGeneration.new(@tournament).generate_matches if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
DoubleEliminationMatchGeneration.new(@tournament).generate_matches if @tournament.tournament_type.include? "Regular Double Elimination"
generation_context = preload_generation_context
seed_wrestlers_in_memory(generation_context)
match_rows = build_match_rows(generation_context)
post_process_match_rows_in_memory(generation_context, match_rows)
persist_generation_rows(generation_context, match_rows)
postMatchCreationActions
PoolToBracketMatchGeneration.new(@tournament).assignLoserNames if @tournament.tournament_type == "Pool to bracket"
ModifiedSixteenManGenerateLoserNames.new(@tournament).assign_loser_names if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
DoubleEliminationGenerateLoserNames.new(@tournament).assign_loser_names if @tournament.tournament_type.include? "Regular Double Elimination"
advance_bye_matches_after_insert
end
def standardStartingActions
@tournament.curently_generating_matches = 1
@tournament.save
WipeTournamentMatches.new(@tournament).setUpMatchGeneration
TournamentSeeding.new(@tournament).set_seeds
end
def preload_generation_context
weights = @tournament.weights.includes(:wrestlers).order(:max).to_a
wrestlers = weights.flat_map(&:wrestlers)
{
weights: weights,
wrestlers: wrestlers,
wrestlers_by_weight_id: wrestlers.group_by(&:weight_id)
}
end
def seed_wrestlers_in_memory(generation_context)
TournamentSeeding.new(@tournament).set_seeds(weights: generation_context[:weights], persist: false)
end
def build_match_rows(generation_context)
return PoolToBracketMatchGeneration.new(
@tournament,
weights: generation_context[:weights],
wrestlers_by_weight_id: generation_context[:wrestlers_by_weight_id]
).generatePoolToBracketMatches if @tournament.tournament_type == "Pool to bracket"
return ModifiedSixteenManMatchGeneration.new(@tournament, weights: generation_context[:weights]).generate_matches if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
return DoubleEliminationMatchGeneration.new(@tournament, weights: generation_context[:weights]).generate_matches if @tournament.tournament_type.include? "Regular Double Elimination"
[]
end
def persist_generation_rows(generation_context, match_rows)
persist_wrestlers(generation_context[:wrestlers])
persist_matches(match_rows)
end
def post_process_match_rows_in_memory(generation_context, match_rows)
move_finals_rows_to_last_round(match_rows) unless @tournament.tournament_type.include?("Regular Double Elimination")
assign_bouts_in_memory(match_rows, generation_context[:weights])
assign_loser_names_in_memory(generation_context, match_rows)
assign_bye_outcomes_in_memory(generation_context, match_rows)
end
def persist_wrestlers(wrestlers)
return if wrestlers.blank?
timestamp = Time.current
rows = wrestlers.map do |w|
{
id: w.id,
bracket_line: w.bracket_line,
pool: w.pool,
updated_at: timestamp
}
end
Wrestler.upsert_all(rows)
end
def persist_matches(match_rows)
return if match_rows.blank?
timestamp = Time.current
rows_with_timestamps = match_rows.map do |row|
row.to_h.symbolize_keys.merge(created_at: timestamp, updated_at: timestamp)
end
all_keys = rows_with_timestamps.flat_map(&:keys).uniq
normalized_rows = rows_with_timestamps.map do |row|
all_keys.each_with_object({}) { |key, normalized| normalized[key] = row[key] }
end
Match.insert_all!(normalized_rows)
end
def postMatchCreationActions
moveFinalsMatchesToLastRound if ! @tournament.tournament_type.include? "Regular Double Elimination"
assignBouts
@tournament.reset_and_fill_bout_board
@tournament.curently_generating_matches = nil
@tournament.save!
Tournament.broadcast_up_matches_board(@tournament.id)
end
def move_finals_rows_to_last_round(match_rows)
finals_round = match_rows.map { |row| row[:round] }.compact.max
return unless finals_round
match_rows.each do |row|
row[:round] = finals_round if ["1/2", "3/4", "5/6", "7/8"].include?(row[:bracket_position])
end
end
def assign_bouts_in_memory(match_rows, weights)
bout_counts = Hash.new(0)
weight_max_by_id = weights.each_with_object({}) { |w, memo| memo[w.id] = w.max }
match_rows
.sort_by { |row| [row[:round].to_i, weight_max_by_id[row[:weight_id]].to_f, row[:weight_id].to_i, row[:bracket_position_number].to_i] }
.each do |row|
round = row[:round].to_i
row[:bout_number] = round * 1000 + bout_counts[round]
bout_counts[round] += 1
end
end
def assign_loser_names_in_memory(generation_context, match_rows)
if @tournament.tournament_type == "Pool to bracket"
service = PoolToBracketGenerateLoserNames.new(@tournament)
generation_context[:weights].each { |weight| service.assign_loser_names_in_memory(weight, match_rows) }
elsif @tournament.tournament_type.include?("Modified 16 Man Double Elimination")
service = ModifiedSixteenManGenerateLoserNames.new(@tournament)
generation_context[:weights].each { |weight| service.assign_loser_names_in_memory(weight, match_rows) }
elsif @tournament.tournament_type.include?("Regular Double Elimination")
service = DoubleEliminationGenerateLoserNames.new(@tournament)
generation_context[:weights].each { |weight| service.assign_loser_names_in_memory(weight, match_rows) }
end
end
def assign_bye_outcomes_in_memory(generation_context, match_rows)
if @tournament.tournament_type.include?("Modified 16 Man Double Elimination")
service = ModifiedSixteenManGenerateLoserNames.new(@tournament)
generation_context[:weights].each { |weight| service.assign_bye_outcomes_in_memory(weight, match_rows) }
elsif @tournament.tournament_type.include?("Regular Double Elimination")
service = DoubleEliminationGenerateLoserNames.new(@tournament)
generation_context[:weights].each { |weight| service.assign_bye_outcomes_in_memory(weight, match_rows) }
end
end
def advance_bye_matches_after_insert
Match.where(tournament_id: @tournament.id, finished: 1, win_type: "BYE")
.where.not(winner_id: nil)
.find_each(&:advance_wrestlers)
end
def assignBouts
bout_counts = Hash.new(0)
@tournament.matches.reload
@tournament.matches.sort_by{|m| [m.round, m.weight_max]}.each do |m|
m.bout_number = m.round * 1000 + bout_counts[m.round]
bout_counts[m.round] += 1
m.save!
timestamp = Time.current
ordered_matches = Match.joins(:weight)
.where(tournament_id: @tournament.id)
.order("matches.round ASC, weights.max ASC, matches.id ASC")
.pluck("matches.id", "matches.round")
updates = []
ordered_matches.each do |match_id, round|
updates << {
id: match_id,
bout_number: round * 1000 + bout_counts[round],
updated_at: timestamp
}
bout_counts[round] += 1
end
Match.upsert_all(updates) if updates.any?
end
def moveFinalsMatchesToLastRound
finalsRound = @tournament.reload.total_rounds
finalsMatches = @tournament.matches.reload.select{|m| m.bracket_position == "1/2" || m.bracket_position == "3/4" || m.bracket_position == "5/6" || m.bracket_position == "7/8"}
finalsMatches. each do |m|
m.round = finalsRound
m.save
end
@tournament.matches
.where(bracket_position: ["1/2", "3/4", "5/6", "7/8"])
.update_all(round: finalsRound, updated_at: Time.current)
end
def unAssignMats
matches = @tournament.matches.reload
matches.each do |m|
m.mat_id = nil
m.save!
end
@tournament.matches.update_all(mat_id: nil, updated_at: Time.current)
end
def unAssignBouts
bout_counts = Hash.new(0)
@tournament.matches.each do |m|
m.bout_number = nil
m.save!
end
@tournament.matches.update_all(bout_number: nil, updated_at: Time.current)
end
end

View File

@@ -1,95 +1,91 @@
class ModifiedSixteenManGenerateLoserNames
def initialize( tournament )
@tournament = tournament
def initialize(tournament)
@tournament = tournament
end
# Compatibility wrapper. Returns transformed rows and does not persist.
def assign_loser_names(match_rows = nil)
rows = match_rows || @tournament.matches.where(tournament_id: @tournament.id).map { |m| m.attributes.symbolize_keys }
@tournament.weights.each do |weight|
assign_loser_names_in_memory(weight, rows)
assign_bye_outcomes_in_memory(weight, rows)
end
rows
end
def assign_loser_names_in_memory(weight, match_rows)
rows = match_rows.select { |row| row[:weight_id] == weight.id }
round_16 = rows.select { |r| r[:bracket_position] == "Bracket Round of 16" }
conso_8 = rows.select { |r| r[:bracket_position] == "Conso Round of 8" }.sort_by { |r| r[:bracket_position_number] }
conso_8.each do |row|
if row[:bracket_position_number] == 1
m1 = round_16.find { |m| m[:bracket_position_number] == 1 }
m2 = round_16.find { |m| m[:bracket_position_number] == 2 }
row[:loser1_name] = "Loser of #{m1[:bout_number]}" if m1
row[:loser2_name] = "Loser of #{m2[:bout_number]}" if m2
elsif row[:bracket_position_number] == 2
m3 = round_16.find { |m| m[:bracket_position_number] == 3 }
m4 = round_16.find { |m| m[:bracket_position_number] == 4 }
row[:loser1_name] = "Loser of #{m3[:bout_number]}" if m3
row[:loser2_name] = "Loser of #{m4[:bout_number]}" if m4
elsif row[:bracket_position_number] == 3
m5 = round_16.find { |m| m[:bracket_position_number] == 5 }
m6 = round_16.find { |m| m[:bracket_position_number] == 6 }
row[:loser1_name] = "Loser of #{m5[:bout_number]}" if m5
row[:loser2_name] = "Loser of #{m6[:bout_number]}" if m6
elsif row[:bracket_position_number] == 4
m7 = round_16.find { |m| m[:bracket_position_number] == 7 }
m8 = round_16.find { |m| m[:bracket_position_number] == 8 }
row[:loser1_name] = "Loser of #{m7[:bout_number]}" if m7
row[:loser2_name] = "Loser of #{m8[:bout_number]}" if m8
end
end
def assign_loser_names
matches_by_weight = nil
@tournament.weights.each do |w|
matches_by_weight = @tournament.matches.where(weight_id: w.id)
conso_round_2(matches_by_weight)
conso_round_3(matches_by_weight)
third_fourth(matches_by_weight)
seventh_eighth(matches_by_weight)
save_matches(matches_by_weight)
matches_by_weight = @tournament.matches.where(weight_id: w.id).reload
advance_bye_matches_championship(matches_by_weight)
save_matches(matches_by_weight)
quarters = rows.select { |r| r[:bracket_position] == "Quarter" }
conso_quarters = rows.select { |r| r[:bracket_position] == "Conso Quarter" }.sort_by { |r| r[:bracket_position_number] }
conso_quarters.each do |row|
source = case row[:bracket_position_number]
when 1 then quarters.find { |q| q[:bracket_position_number] == 4 }
when 2 then quarters.find { |q| q[:bracket_position_number] == 3 }
when 3 then quarters.find { |q| q[:bracket_position_number] == 2 }
when 4 then quarters.find { |q| q[:bracket_position_number] == 1 }
end
row[:loser1_name] = "Loser of #{source[:bout_number]}" if source
end
end
def conso_round_2(matches)
matches.select{|m| m.bracket_position == "Conso Round of 8"}.sort_by{|m| m.bracket_position_number}.each do |match|
if match.bracket_position_number == 1
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 1 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
elsif match.bracket_position_number == 2
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 3 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
elsif match.bracket_position_number == 3
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 5 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 6 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
elsif match.bracket_position_number == 4
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 7 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 8 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
end
end
end
semis = rows.select { |r| r[:bracket_position] == "Semis" }
third_fourth = rows.find { |r| r[:bracket_position] == "3/4" }
if third_fourth
third_fourth[:loser1_name] = "Loser of #{semis.first[:bout_number]}" if semis.first
third_fourth[:loser2_name] = "Loser of #{semis.second[:bout_number]}" if semis.second
end
def conso_round_3(matches)
matches.select{|m| m.bracket_position == "Conso Quarter"}.sort_by{|m| m.bracket_position_number}.each do |match|
if match.bracket_position_number == 1
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.bracket_position == "Quarter"}.first.bout_number}"
elsif match.bracket_position_number == 2
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 3 and m.bracket_position == "Quarter"}.first.bout_number}"
elsif match.bracket_position_number == 3
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.bracket_position == "Quarter"}.first.bout_number}"
elsif match.bracket_position_number == 4
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 1 and m.bracket_position == "Quarter"}.first.bout_number}"
end
end
end
conso_semis = rows.select { |r| r[:bracket_position] == "Conso Semis" }
seventh_eighth = rows.find { |r| r[:bracket_position] == "7/8" }
if seventh_eighth
seventh_eighth[:loser1_name] = "Loser of #{conso_semis.first[:bout_number]}" if conso_semis.first
seventh_eighth[:loser2_name] = "Loser of #{conso_semis.second[:bout_number]}" if conso_semis.second
end
end
def third_fourth(matches)
matches.select{|m| m.bracket_position == "3/4"}.sort_by{|m| m.bracket_position_number}.each do |match|
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position == "Semis"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position == "Semis"}.second.bout_number}"
end
end
def seventh_eighth(matches)
matches.select{|m| m.bracket_position == "7/8"}.sort_by{|m| m.bracket_position_number}.each do |match|
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position == "Conso Semis"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position == "Conso Semis"}.second.bout_number}"
end
end
def assign_bye_outcomes_in_memory(weight, match_rows)
rows = match_rows.select { |r| r[:weight_id] == weight.id && r[:bracket_position] == "Bracket Round of 16" }
rows.each { |row| apply_bye_to_row(row) }
end
def advance_bye_matches_championship(matches)
matches.select{|m| m.bracket_position == "Bracket Round of 16"}.sort_by{|m| m.bracket_position_number}.each do |match|
if match.w1 == nil or match.w2 == nil
match.finished = 1
match.win_type = "BYE"
if match.w1 != nil
match.winner_id = match.w1
match.loser2_name = "BYE"
match.score = ""
match.save
match.advance_wrestlers
elsif match.w2 != nil
match.winner_id = match.w2
match.loser1_name = "BYE"
match.score = ""
match.save
match.advance_wrestlers
end
end
end
end
def save_matches(matches)
matches.each do |m|
m.save!
end
end
end
def apply_bye_to_row(row)
return unless [row[:w1], row[:w2]].compact.size == 1
row[:finished] = 1
row[:win_type] = "BYE"
if row[:w1]
row[:winner_id] = row[:w1]
row[:loser2_name] = "BYE"
else
row[:winner_id] = row[:w2]
row[:loser1_name] = "BYE"
end
row[:score] = ""
end
end

View File

@@ -1,70 +1,75 @@
class ModifiedSixteenManMatchGeneration
def initialize( tournament )
def initialize( tournament, weights: nil )
@tournament = tournament
@number_of_placers = @tournament.number_of_placers
@weights = weights
end
def generate_matches
@tournament.weights.each do |weight|
generate_matches_for_weight(weight)
rows = []
generation_weights.each do |weight|
rows.concat(generate_match_rows_for_weight(weight))
end
rows
end
def generate_matches_for_weight(weight)
round_one(weight)
round_two(weight)
round_three(weight)
round_four(weight)
round_five(weight)
def generate_match_rows_for_weight(weight)
rows = []
round_one(weight, rows)
round_two(weight, rows)
round_three(weight, rows)
round_four(weight, rows)
round_five(weight, rows)
rows
end
def round_one(weight)
create_matchup_from_seed(1,16, "Bracket Round of 16", 1, 1,weight)
create_matchup_from_seed(8,9, "Bracket Round of 16", 2, 1,weight)
create_matchup_from_seed(5,12, "Bracket Round of 16", 3, 1,weight)
create_matchup_from_seed(4,14, "Bracket Round of 16", 4, 1,weight)
create_matchup_from_seed(3,13, "Bracket Round of 16", 5, 1,weight)
create_matchup_from_seed(6,11, "Bracket Round of 16", 6, 1,weight)
create_matchup_from_seed(7,10, "Bracket Round of 16", 7, 1,weight)
create_matchup_from_seed(2,15, "Bracket Round of 16", 8, 1,weight)
def round_one(weight, rows)
rows << create_matchup_from_seed(1,16, "Bracket Round of 16", 1, 1,weight)
rows << create_matchup_from_seed(8,9, "Bracket Round of 16", 2, 1,weight)
rows << create_matchup_from_seed(5,12, "Bracket Round of 16", 3, 1,weight)
rows << create_matchup_from_seed(4,14, "Bracket Round of 16", 4, 1,weight)
rows << create_matchup_from_seed(3,13, "Bracket Round of 16", 5, 1,weight)
rows << create_matchup_from_seed(6,11, "Bracket Round of 16", 6, 1,weight)
rows << create_matchup_from_seed(7,10, "Bracket Round of 16", 7, 1,weight)
rows << create_matchup_from_seed(2,15, "Bracket Round of 16", 8, 1,weight)
end
def round_two(weight)
create_matchup(nil,nil,"Quarter",1,2,weight)
create_matchup(nil,nil,"Quarter",2,2,weight)
create_matchup(nil,nil,"Quarter",3,2,weight)
create_matchup(nil,nil,"Quarter",4,2,weight)
create_matchup(nil,nil,"Conso Round of 8",1,2,weight)
create_matchup(nil,nil,"Conso Round of 8",2,2,weight)
create_matchup(nil,nil,"Conso Round of 8",3,2,weight)
create_matchup(nil,nil,"Conso Round of 8",4,2,weight)
def round_two(weight, rows)
rows << create_matchup(nil,nil,"Quarter",1,2,weight)
rows << create_matchup(nil,nil,"Quarter",2,2,weight)
rows << create_matchup(nil,nil,"Quarter",3,2,weight)
rows << create_matchup(nil,nil,"Quarter",4,2,weight)
rows << create_matchup(nil,nil,"Conso Round of 8",1,2,weight)
rows << create_matchup(nil,nil,"Conso Round of 8",2,2,weight)
rows << create_matchup(nil,nil,"Conso Round of 8",3,2,weight)
rows << create_matchup(nil,nil,"Conso Round of 8",4,2,weight)
end
def round_three(weight)
create_matchup(nil,nil,"Semis",1,3,weight)
create_matchup(nil,nil,"Semis",2,3,weight)
create_matchup(nil,nil,"Conso Quarter",1,3,weight)
create_matchup(nil,nil,"Conso Quarter",2,3,weight)
create_matchup(nil,nil,"Conso Quarter",3,3,weight)
create_matchup(nil,nil,"Conso Quarter",4,3,weight)
def round_three(weight, rows)
rows << create_matchup(nil,nil,"Semis",1,3,weight)
rows << create_matchup(nil,nil,"Semis",2,3,weight)
rows << create_matchup(nil,nil,"Conso Quarter",1,3,weight)
rows << create_matchup(nil,nil,"Conso Quarter",2,3,weight)
rows << create_matchup(nil,nil,"Conso Quarter",3,3,weight)
rows << create_matchup(nil,nil,"Conso Quarter",4,3,weight)
end
def round_four(weight)
create_matchup(nil,nil,"Conso Semis",1,4,weight)
create_matchup(nil,nil,"Conso Semis",2,4,weight)
def round_four(weight, rows)
rows << create_matchup(nil,nil,"Conso Semis",1,4,weight)
rows << create_matchup(nil,nil,"Conso Semis",2,4,weight)
end
def round_five(weight)
create_matchup(nil,nil,"1/2",1,5,weight)
create_matchup(nil,nil,"3/4",1,5,weight)
create_matchup(nil,nil,"5/6",1,5,weight)
def round_five(weight, rows)
rows << create_matchup(nil,nil,"1/2",1,5,weight)
rows << create_matchup(nil,nil,"3/4",1,5,weight)
rows << create_matchup(nil,nil,"5/6",1,5,weight)
if @number_of_placers >= 8
create_matchup(nil,nil,"7/8",1,5,weight)
rows << create_matchup(nil,nil,"7/8",1,5,weight)
end
end
def wrestler_with_seed(seed,weight)
wrestler = Wrestler.where("weight_id = ? and bracket_line = ?", weight.id, seed).first
wrestler = weight.wrestlers.find { |w| w.bracket_line == seed }
if wrestler
return wrestler.id
else
@@ -79,13 +84,18 @@ class ModifiedSixteenManMatchGeneration
end
def create_matchup(w1, w2, bracket_position, bracket_position_number,round,weight)
@tournament.matches.create(
{
w1: w1,
w2: w2,
tournament_id: @tournament.id,
weight_id: weight.id,
round: round,
bracket_position: bracket_position,
bracket_position_number: bracket_position_number
)
}
end
end
def generation_weights
@weights || @tournament.weights.to_a
end
end

View File

@@ -12,18 +12,19 @@ class PoolBracketGeneration
end
def generateBracketMatches()
@rows = []
if @pool_bracket_type == "twoPoolsToSemi"
return twoPoolsToSemi()
twoPoolsToSemi()
elsif @pool_bracket_type == "twoPoolsToFinal"
return twoPoolsToFinal()
twoPoolsToFinal()
elsif @pool_bracket_type == "fourPoolsToQuarter"
return fourPoolsToQuarter()
fourPoolsToQuarter()
elsif @pool_bracket_type == "fourPoolsToSemi"
return fourPoolsToSemi()
fourPoolsToSemi()
elsif @pool_bracket_type == "eightPoolsToQuarter"
return eightPoolsToQuarter()
eightPoolsToQuarter()
end
return []
@rows
end
def twoPoolsToSemi()
@@ -86,14 +87,15 @@ class PoolBracketGeneration
end
def createMatchup(w1_name, w2_name, bracket_position, bracket_position_number)
@tournament.matches.create(
@rows << {
loser1_name: w1_name,
loser2_name: w2_name,
tournament_id: @tournament.id,
weight_id: @weight.id,
round: @round,
bracket_position: bracket_position,
bracket_position_number: bracket_position_number
)
}
end
end

View File

@@ -1,35 +1,46 @@
class PoolGeneration
def initialize(weight)
def initialize(weight, wrestlers: nil)
@weight = weight
@tournament = @weight.tournament
@pool = 1
@wrestlers = wrestlers
end
def generatePools
GeneratePoolNumbers.new(@weight).savePoolNumbers
GeneratePoolNumbers.new(@weight).savePoolNumbers(wrestlers: wrestlers_for_weight, persist: false)
rows = []
pools = @weight.pools
while @pool <= pools
roundRobin
rows.concat(roundRobin)
@pool += 1
end
rows
end
def roundRobin
wrestlers = @weight.wrestlers_in_pool(@pool)
rows = []
wrestlers = wrestlers_for_weight.select { |w| w.pool == @pool }
pool_matches = RoundRobinTournament.schedule(wrestlers).reverse
pool_matches.each_with_index do |b, index|
round = index + 1
bouts = b.map
bouts.each do |bout|
if bout[0] != nil and bout[1] != nil
@tournament.matches.create(
rows << {
w1: bout[0].id,
w2: bout[1].id,
tournament_id: @tournament.id,
weight_id: @weight.id,
bracket_position: "Pool",
round: round)
round: round
}
end
end
end
rows
end
def wrestlers_for_weight
@wrestlers || @weight.wrestlers.to_a
end
end

View File

@@ -1,80 +1,97 @@
class PoolToBracketGenerateLoserNames
def initialize( tournament )
@tournament = tournament
end
def assignLoserNamesWeight(weight)
matches_by_weight = @tournament.matches.where(weight_id: weight.id)
if weight.pool_bracket_type == "twoPoolsToSemi"
twoPoolsToSemiLoser(matches_by_weight)
elsif (weight.pool_bracket_type == "fourPoolsToQuarter") or (weight.pool_bracket_type == "eightPoolsToQuarter")
fourPoolsToQuarterLoser(matches_by_weight)
elsif weight.pool_bracket_type == "fourPoolsToSemi"
fourPoolsToSemiLoser(matches_by_weight)
end
saveMatches(matches_by_weight)
def initialize(tournament)
@tournament = tournament
end
def assignLoserNames
matches_by_weight = nil
@tournament.weights.each do |w|
matches_by_weight = @tournament.matches.where(weight_id: w.id)
if w.pool_bracket_type == "twoPoolsToSemi"
twoPoolsToSemiLoser(matches_by_weight)
elsif (w.pool_bracket_type == "fourPoolsToQuarter") or (w.pool_bracket_type == "eightPoolsToQuarter")
fourPoolsToQuarterLoser(matches_by_weight)
elsif w.pool_bracket_type == "fourPoolsToSemi"
fourPoolsToSemiLoser(matches_by_weight)
end
saveMatches(matches_by_weight)
end
end
def twoPoolsToSemiLoser(matches_by_weight)
match1 = matches_by_weight.select{|m| m.loser1_name == "Winner Pool 1"}.first
match2 = matches_by_weight.select{|m| m.loser1_name == "Winner Pool 2"}.first
matchChange = matches_by_weight.select{|m| m.bracket_position == "3/4"}.first
matchChange.loser1_name = "Loser of #{match1.bout_number}"
matchChange.loser2_name = "Loser of #{match2.bout_number}"
# Compatibility wrapper. Returns transformed rows and does not persist.
def assignLoserNamesWeight(weight, match_rows = nil)
rows = match_rows || @tournament.matches.where(weight_id: weight.id).map { |m| m.attributes.symbolize_keys }
assign_loser_names_in_memory(weight, rows)
rows
end
def fourPoolsToQuarterLoser(matches_by_weight)
quarters = matches_by_weight.select{|m| m.bracket_position == "Quarter"}
consoSemis = matches_by_weight.select{|m| m.bracket_position == "Conso Semis"}
semis = matches_by_weight.select{|m| m.bracket_position == "Semis"}
thirdFourth = matches_by_weight.select{|m| m.bracket_position == "3/4"}.first
seventhEighth = matches_by_weight.select{|m| m.bracket_position == "7/8"}.first
consoSemis.each do |m|
if m.bracket_position_number == 1
m.loser1_name = "Loser of #{quarters.select{|m| m.bracket_position_number == 1}.first.bout_number}"
m.loser2_name = "Loser of #{quarters.select{|m| m.bracket_position_number == 2}.first.bout_number}"
elsif m.bracket_position_number == 2
m.loser1_name = "Loser of #{quarters.select{|m| m.bracket_position_number == 3}.first.bout_number}"
m.loser2_name = "Loser of #{quarters.select{|m| m.bracket_position_number == 4}.first.bout_number}"
# Compatibility wrapper. Returns transformed rows and does not persist.
def assignLoserNames
@tournament.weights.each_with_object([]) do |weight, all_rows|
all_rows.concat(assignLoserNamesWeight(weight))
end
end
def assign_loser_names_in_memory(weight, match_rows)
rows = match_rows.select { |row| row[:weight_id] == weight.id }
if weight.pool_bracket_type == "twoPoolsToSemi"
two_pools_to_semi_loser_rows(rows)
elsif (weight.pool_bracket_type == "fourPoolsToQuarter") || (weight.pool_bracket_type == "eightPoolsToQuarter")
four_pools_to_quarter_loser_rows(rows)
elsif weight.pool_bracket_type == "fourPoolsToSemi"
four_pools_to_semi_loser_rows(rows)
end
end
def two_pools_to_semi_loser_rows(rows)
match1 = rows.find { |m| m[:loser1_name] == "Winner Pool 1" }
match2 = rows.find { |m| m[:loser1_name] == "Winner Pool 2" }
match_change = rows.find { |m| m[:bracket_position] == "3/4" }
return unless match1 && match2 && match_change
match_change[:loser1_name] = "Loser of #{match1[:bout_number]}"
match_change[:loser2_name] = "Loser of #{match2[:bout_number]}"
end
def four_pools_to_quarter_loser_rows(rows)
quarters = rows.select { |m| m[:bracket_position] == "Quarter" }
conso_semis = rows.select { |m| m[:bracket_position] == "Conso Semis" }
semis = rows.select { |m| m[:bracket_position] == "Semis" }
third_fourth = rows.find { |m| m[:bracket_position] == "3/4" }
seventh_eighth = rows.find { |m| m[:bracket_position] == "7/8" }
conso_semis.each do |m|
if m[:bracket_position_number] == 1
q1 = quarters.find { |q| q[:bracket_position_number] == 1 }
q2 = quarters.find { |q| q[:bracket_position_number] == 2 }
m[:loser1_name] = "Loser of #{q1[:bout_number]}" if q1
m[:loser2_name] = "Loser of #{q2[:bout_number]}" if q2
elsif m[:bracket_position_number] == 2
q3 = quarters.find { |q| q[:bracket_position_number] == 3 }
q4 = quarters.find { |q| q[:bracket_position_number] == 4 }
m[:loser1_name] = "Loser of #{q3[:bout_number]}" if q3
m[:loser2_name] = "Loser of #{q4[:bout_number]}" if q4
end
end
thirdFourth.loser1_name = "Loser of #{semis.select{|m| m.bracket_position_number == 1}.first.bout_number}"
thirdFourth.loser2_name = "Loser of #{semis.select{|m| m.bracket_position_number == 2}.first.bout_number}"
consoSemis = matches_by_weight.select{|m| m.bracket_position == "Conso Semis"}
seventhEighth.loser1_name = "Loser of #{consoSemis.select{|m| m.bracket_position_number == 1}.first.bout_number}"
seventhEighth.loser2_name = "Loser of #{consoSemis.select{|m| m.bracket_position_number == 2}.first.bout_number}"
if third_fourth
s1 = semis.find { |s| s[:bracket_position_number] == 1 }
s2 = semis.find { |s| s[:bracket_position_number] == 2 }
third_fourth[:loser1_name] = "Loser of #{s1[:bout_number]}" if s1
third_fourth[:loser2_name] = "Loser of #{s2[:bout_number]}" if s2
end
if seventh_eighth
c1 = conso_semis.find { |c| c[:bracket_position_number] == 1 }
c2 = conso_semis.find { |c| c[:bracket_position_number] == 2 }
seventh_eighth[:loser1_name] = "Loser of #{c1[:bout_number]}" if c1
seventh_eighth[:loser2_name] = "Loser of #{c2[:bout_number]}" if c2
end
end
def fourPoolsToSemiLoser(matches_by_weight)
semis = matches_by_weight.select{|m| m.bracket_position == "Semis"}
thirdFourth = matches_by_weight.select{|m| m.bracket_position == "3/4"}.first
consoSemis = matches_by_weight.select{|m| m.bracket_position == "Conso Semis"}
seventhEighth = matches_by_weight.select{|m| m.bracket_position == "7/8"}.first
thirdFourth.loser1_name = "Loser of #{semis.select{|m| m.bracket_position_number == 1}.first.bout_number}"
thirdFourth.loser2_name = "Loser of #{semis.select{|m| m.bracket_position_number == 2}.first.bout_number}"
seventhEighth.loser1_name = "Loser of #{consoSemis.select{|m| m.bracket_position_number == 1}.first.bout_number}"
seventhEighth.loser2_name = "Loser of #{consoSemis.select{|m| m.bracket_position_number == 2}.first.bout_number}"
def four_pools_to_semi_loser_rows(rows)
semis = rows.select { |m| m[:bracket_position] == "Semis" }
conso_semis = rows.select { |m| m[:bracket_position] == "Conso Semis" }
third_fourth = rows.find { |m| m[:bracket_position] == "3/4" }
seventh_eighth = rows.find { |m| m[:bracket_position] == "7/8" }
if third_fourth
s1 = semis.find { |s| s[:bracket_position_number] == 1 }
s2 = semis.find { |s| s[:bracket_position_number] == 2 }
third_fourth[:loser1_name] = "Loser of #{s1[:bout_number]}" if s1
third_fourth[:loser2_name] = "Loser of #{s2[:bout_number]}" if s2
end
if seventh_eighth
c1 = conso_semis.find { |c| c[:bracket_position_number] == 1 }
c2 = conso_semis.find { |c| c[:bracket_position_number] == 2 }
seventh_eighth[:loser1_name] = "Loser of #{c1[:bout_number]}" if c1
seventh_eighth[:loser2_name] = "Loser of #{c2[:bout_number]}" if c2
end
end
def saveMatches(matches)
matches.each do |m|
m.save!
end
end
end
end

View File

@@ -1,44 +1,92 @@
class PoolToBracketMatchGeneration
def initialize( tournament )
def initialize(tournament, weights: nil, wrestlers_by_weight_id: nil)
@tournament = tournament
@weights = weights
@wrestlers_by_weight_id = wrestlers_by_weight_id
end
def generatePoolToBracketMatches
@tournament.weights.order(:max).each do |weight|
PoolGeneration.new(weight).generatePools()
last_match = @tournament.matches.where(weight: weight).order(round: :desc).limit(1).first
highest_round = last_match.round
PoolBracketGeneration.new(weight, highest_round).generateBracketMatches()
rows = []
generation_weights.each do |weight|
wrestlers = wrestlers_for_weight(weight)
pool_rows = PoolGeneration.new(weight, wrestlers: wrestlers).generatePools
rows.concat(pool_rows)
highest_round = pool_rows.map { |row| row[:round] }.max || 0
bracket_rows = PoolBracketGeneration.new(weight, highest_round).generateBracketMatches
rows.concat(bracket_rows)
end
movePoolSeedsToFinalPoolRound
movePoolSeedsToFinalPoolRound(rows)
rows
end
def movePoolSeedsToFinalPoolRound
@tournament.weights.each do |w|
setOriginalSeedsToWrestleLastPoolRound(w)
def movePoolSeedsToFinalPoolRound(match_rows)
generation_weights.each do |w|
setOriginalSeedsToWrestleLastPoolRound(w, match_rows)
end
end
def setOriginalSeedsToWrestleLastPoolRound(weight)
def setOriginalSeedsToWrestleLastPoolRound(weight, match_rows)
pool = 1
until pool > weight.pools
wrestler1 = weight.pool_wrestlers_sorted_by_bracket_line(pool).first
wrestler2 = weight.pool_wrestlers_sorted_by_bracket_line(pool).second
match = wrestler1.pool_matches.sort_by{|m| m.round}.last
if match.w1 != wrestler2.id or match.w2 != wrestler2.id
if match.w1 == wrestler1.id
SwapWrestlers.new.swap_wrestlers_bracket_lines(match.w2,wrestler2.id)
elsif match.w2 == wrestler1.id
SwapWrestlers.new.swap_wrestlers_bracket_lines(match.w1,wrestler2.id)
end
wrestlers = wrestlers_for_weight(weight)
weight_pools = weight.pools
until pool > weight_pools
pool_wrestlers = wrestlers.select { |w| w.pool == pool }.sort_by(&:bracket_line)
wrestler1 = pool_wrestlers.first
wrestler2 = pool_wrestlers.second
if wrestler1 && wrestler2
pool_matches = match_rows.select { |row| row[:weight_id] == weight.id && row[:bracket_position] == "Pool" && (row[:w1] == wrestler1.id || row[:w2] == wrestler1.id) }
match = pool_matches.max_by { |row| row[:round] }
if match && (match[:w1] != wrestler2.id || match[:w2] != wrestler2.id)
if match[:w1] == wrestler1.id
swap_wrestlers_in_memory(match_rows, wrestlers, match[:w2], wrestler2.id)
elsif match[:w2] == wrestler1.id
swap_wrestlers_in_memory(match_rows, wrestlers, match[:w1], wrestler2.id)
end
end
end
pool += 1
end
end
def swap_wrestlers_in_memory(match_rows, wrestlers, wrestler1_id, wrestler2_id)
w1 = wrestlers.find { |w| w.id == wrestler1_id }
w2 = wrestlers.find { |w| w.id == wrestler2_id }
return unless w1 && w2
w1_bracket_line, w1_pool = w1.bracket_line, w1.pool
w1.bracket_line, w1.pool = w2.bracket_line, w2.pool
w2.bracket_line, w2.pool = w1_bracket_line, w1_pool
swap_match_rows(match_rows, wrestler1_id, wrestler2_id)
end
def swap_match_rows(match_rows, wrestler1_id, wrestler2_id)
match_rows.each do |row|
row[:w1] = swap_id(row[:w1], wrestler1_id, wrestler2_id)
row[:w2] = swap_id(row[:w2], wrestler1_id, wrestler2_id)
row[:winner_id] = swap_id(row[:winner_id], wrestler1_id, wrestler2_id)
end
end
def swap_id(value, wrestler1_id, wrestler2_id)
return wrestler2_id if value == wrestler1_id
return wrestler1_id if value == wrestler2_id
value
end
def generation_weights
@weights || @tournament.weights.order(:max).to_a
end
def wrestlers_for_weight(weight)
@wrestlers_by_weight_id&.fetch(weight.id, nil) || weight.wrestlers.to_a
end
def assignLoserNames
PoolToBracketGenerateLoserNames.new(@tournament).assignLoserNames
end
end
end

View File

@@ -37,7 +37,11 @@ class TournamentBackupService
attributes: @tournament.attributes,
schools: @tournament.schools.map(&:attributes),
weights: @tournament.weights.map(&:attributes),
mats: @tournament.mats.map(&:attributes),
mats: @tournament.mats.map do |mat|
mat.attributes.merge(
"queue_bout_numbers" => mat.queue_matches.map { |match| match&.bout_number }
)
end,
mat_assignment_rules: @tournament.mat_assignment_rules.map do |rule|
rule.attributes.merge(
mat: Mat.find_by(id: rule.mat_id)&.attributes.slice("name"),

View File

@@ -3,30 +3,36 @@ class TournamentSeeding
@tournament = tournament
end
def set_seeds
@tournament.weights.each do |weight|
def set_seeds(weights: nil, persist: true)
weights_to_seed = weights || @tournament.weights.includes(:wrestlers)
updated_wrestlers = []
weights_to_seed.each do |weight|
wrestlers = weight.wrestlers
bracket_size = weight.calculate_bracket_size
wrestlers = reset_bracket_line_for_lines_higher_than_bracket_size(wrestlers, bracket_size)
wrestlers = set_original_seed_to_bracket_line(wrestlers)
wrestlers = random_seeding(wrestlers, bracket_size)
wrestlers.each(&:save)
updated_wrestlers.concat(wrestlers)
end
persist_bracket_lines(updated_wrestlers) if persist
updated_wrestlers
end
def random_seeding(wrestlers, bracket_size)
half_of_bracket = bracket_size / 2
available_bracket_lines = (1..bracket_size).to_a
first_half_available_bracket_lines = (1..half_of_bracket).to_a
# remove bracket lines that are taken from available_bracket_lines
wrestlers_with_bracket_lines = wrestlers.select{|w| w.bracket_line != nil }
wrestlers_with_bracket_lines.each do |wrestler|
available_bracket_lines.delete(wrestler.bracket_line)
first_half_available_bracket_lines.delete(wrestler.bracket_line)
end
available_bracket_lines_to_use = set_random_seeding_bracket_line_order(available_bracket_lines)
wrestlers_without_bracket_lines = wrestlers.select{|w| w.bracket_line == nil }
if @tournament.tournament_type == "Pool to bracket"
wrestlers_without_bracket_lines.shuffle.each do |wrestler|
@@ -38,15 +44,10 @@ class TournamentSeeding
else
# Iterrate over the list randomly
wrestlers_without_bracket_lines.shuffle.each do |wrestler|
if first_half_available_bracket_lines.size > 0
random_available_bracket_line = first_half_available_bracket_lines.sample
wrestler.bracket_line = random_available_bracket_line
available_bracket_lines.delete(random_available_bracket_line)
first_half_available_bracket_lines.delete(random_available_bracket_line)
else
random_available_bracket_line = available_bracket_lines.sample
wrestler.bracket_line = random_available_bracket_line
available_bracket_lines.delete(random_available_bracket_line)
if available_bracket_lines_to_use.size > 0
bracket_line_to_use = available_bracket_lines_to_use.first
wrestler.bracket_line = bracket_line_to_use
available_bracket_lines_to_use.delete(bracket_line_to_use)
end
end
end
@@ -81,4 +82,39 @@ class TournamentSeeding
end
return wrestlers
end
end
private
def set_random_seeding_bracket_line_order(available_bracket_lines)
# This method prevents double BYEs in round 1
# It also evenly distributes matches from the top half of the bracket to the bottom half
# It does both of these while keeping the randomness of the line assignment
odd_or_even = [0, 1]
odd_or_even_sample = odd_or_even.sample
# sort by odd or even based on the sample above
if odd_or_even_sample == 1
# odd numbers first
result = available_bracket_lines.sort_by { |n| n.even? ? 1 : 0 }
else
# even numbers first
result = available_bracket_lines.sort_by { |n| n.odd? ? 1 : 0 }
end
result
end
def persist_bracket_lines(wrestlers)
return if wrestlers.blank?
timestamp = Time.current
updates = wrestlers.map do |wrestler|
{
id: wrestler.id,
bracket_line: wrestler.bracket_line,
updated_at: timestamp
}
end
Wrestler.upsert_all(updates)
end
end

View File

@@ -14,10 +14,10 @@ class WipeTournamentMatches
end
def wipeMatches
@tournament.matches.destroy_all
@tournament.destroy_all_matches
end
def resetSchoolScores
@tournament.schools.update_all("score = 0.0")
end
end
end

View File

@@ -41,20 +41,20 @@ class WrestlingdevImporter
@tournament.matches.destroy_all
@tournament.mat_assignment_rules.destroy_all # Explicitly destroy rules (might be redundant if Mat cascades)
@tournament.delegates.destroy_all
@tournament.tournament_backups.destroy_all
@tournament.tournament_job_statuses.destroy_all
# Note: Teampointadjusts are deleted via School/Wrestler cascade
end
def parse_data
parse_tournament(@import_data["tournament"]["attributes"])
parse_schools(@import_data["tournament"]["schools"])
parse_weights(@import_data["tournament"]["weights"])
parse_mats(@import_data["tournament"]["mats"])
parse_wrestlers(@import_data["tournament"]["wrestlers"])
parse_matches(@import_data["tournament"]["matches"])
parse_mat_assignment_rules(@import_data["tournament"]["mat_assignment_rules"])
end
def parse_data
parse_tournament(@import_data["tournament"]["attributes"])
parse_schools(@import_data["tournament"]["schools"])
parse_weights(@import_data["tournament"]["weights"])
parse_mats(@import_data["tournament"]["mats"])
parse_wrestlers(@import_data["tournament"]["wrestlers"])
parse_matches(@import_data["tournament"]["matches"])
apply_mat_queues
parse_mat_assignment_rules(@import_data["tournament"]["mat_assignment_rules"])
end
def parse_tournament(attributes)
attributes.except!("id")
@@ -75,12 +75,18 @@ class WrestlingdevImporter
end
end
def parse_mats(mats)
mats.each do |mat_attributes|
mat_attributes.except!("id")
Mat.create(mat_attributes.merge(tournament_id: @tournament.id))
end
end
def parse_mats(mats)
@mat_queue_bout_numbers = {}
mats.each do |mat_attributes|
mat_name = mat_attributes["name"]
queue_bout_numbers = mat_attributes["queue_bout_numbers"]
mat_attributes.except!("id", "queue1", "queue2", "queue3", "queue4", "queue_bout_numbers", "tournament_id")
Mat.create(mat_attributes.merge(tournament_id: @tournament.id))
if mat_name && queue_bout_numbers
@mat_queue_bout_numbers[mat_name] = queue_bout_numbers
end
end
end
def parse_mat_assignment_rules(mat_assignment_rules)
mat_assignment_rules.each do |rule_attributes|
@@ -135,9 +141,9 @@ class WrestlingdevImporter
end
end
def parse_matches(matches)
matches.each do |match_attributes|
next unless match_attributes # Skip if match_attributes is nil
def parse_matches(matches)
matches.each do |match_attributes|
next unless match_attributes # Skip if match_attributes is nil
weight = Weight.find_by(max: match_attributes.dig("weight", "max"), tournament_id: @tournament.id)
mat = Mat.find_by(name: match_attributes.dig("mat", "name"), tournament_id: @tournament.id)
@@ -156,6 +162,53 @@ class WrestlingdevImporter
w2: w2&.id,
winner_id: winner&.id
))
end
end
end
end
end
def apply_mat_queues
if @mat_queue_bout_numbers.blank?
Mat.where(tournament_id: @tournament.id).find_each do |mat|
match_ids = mat.matches.where(finished: [nil, 0]).order(:bout_number).limit(4).pluck(:id)
mat.update(
queue1: match_ids[0],
queue2: match_ids[1],
queue3: match_ids[2],
queue4: match_ids[3]
)
end
return
end
@mat_queue_bout_numbers.each do |mat_name, bout_numbers|
mat = Mat.find_by(name: mat_name, tournament_id: @tournament.id)
next unless mat
matches = Array(bout_numbers).map do |bout_number|
Match.find_by(bout_number: bout_number, tournament_id: @tournament.id)
end
mat.update(
queue1: matches[0]&.id,
queue2: matches[1]&.id,
queue3: matches[2]&.id,
queue4: matches[3]&.id
)
matches.compact.each do |match|
match.update(mat_id: mat.id)
end
end
Mat.where(tournament_id: @tournament.id)
.where(queue1: nil, queue2: nil, queue3: nil, queue4: nil)
.find_each do |mat|
match_ids = mat.matches.where(finished: [nil, 0]).order(:bout_number).limit(4).pluck(:id)
mat.update(
queue1: match_ids[0],
queue2: match_ids[1],
queue3: match_ids[2],
queue4: match_ids[3]
)
end
end
end

View File

@@ -3,11 +3,13 @@ class GeneratePoolNumbers
@weight = weight
end
def savePoolNumbers
@weight.wrestlers.each do |wrestler|
def savePoolNumbers(wrestlers: nil, persist: true)
wrestlers_to_update = wrestlers || @weight.wrestlers.to_a
wrestlers_to_update.each do |wrestler|
wrestler.pool = get_wrestler_pool_number(@weight.pools, wrestler.bracket_line)
wrestler.save
end
persist_pool_numbers(wrestlers_to_update) if persist
wrestlers_to_update
end
def get_wrestler_pool_number(number_of_pools, wrestler_seed)
@@ -36,4 +38,20 @@ class GeneratePoolNumbers
pool
end
end
private
def persist_pool_numbers(wrestlers)
return if wrestlers.blank?
timestamp = Time.current
rows = wrestlers.map do |w|
{
id: w.id,
pool: w.pool,
updated_at: timestamp
}
end
Wrestler.upsert_all(rows)
end
end

View File

@@ -54,29 +54,20 @@ class CalculateWrestlerTeamScore
def byePoints
points = 0
if @tournament.tournament_type == "Pool to bracket"
if @wrestler.pool_wins.size >= 1 and @wrestler.has_a_pool_bye == true
if pool_bye_points_eligible?
points += 2
end
end
if @tournament.tournament_type.include? "Regular Double Elimination"
if @wrestler.championship_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "1/2" and m.win_type != "BYE"}.size > 0
# if they have a win in the championship round or if they got a bye all the way to finals and won the finals
points += @wrestler.championship_byes.size * 2
if @tournament.tournament_type.include? "Double Elimination"
if @wrestler.championship_advancement_wins.any? &&
@wrestler.championship_byes.any? &&
any_bye_round_had_wrestled_match?(@wrestler.championship_byes)
points += 2
end
if @wrestler.consolation_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "3/4" and m.win_type != "BYE"}.size > 0
# if they have a win in the consolation round or if they got a bye all the way to 3rd/4th match and won
points += @wrestler.consolation_byes.size * 1
end
end
if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
if @wrestler.championship_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "1/2" and m.win_type != "BYE"}.size > 0
# if they have a win in the championship round or if they got a bye all the way to finals and won the finals
points += @wrestler.championship_byes.size * 2
end
if @wrestler.consolation_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "5/6" and m.win_type != "BYE"}.size > 0
# if they have a win in the consolation round or if they got a bye all the way to 5th/6th match and won
# since the consolation bracket goes to 5/6 in a modified tournament
points += @wrestler.consolation_byes.size * 1
if @wrestler.consolation_advancement_wins.any? &&
@wrestler.consolation_byes.any? &&
any_bye_round_had_wrestled_match?(@wrestler.consolation_byes)
points += 1
end
end
return points
@@ -86,4 +77,30 @@ class CalculateWrestlerTeamScore
(@wrestler.pin_wins.size * 2) + (@wrestler.tech_wins.size * 1.5) + (@wrestler.major_wins.size * 1)
end
private
def pool_bye_points_eligible?
return false unless @wrestler.pool_wins.size >= 1
return false unless @wrestler.weight.pools.to_i > 1
wrestler_pool_size = @wrestler.weight.wrestlers_in_pool(@wrestler.pool).size
largest_pool_size = (1..@wrestler.weight.pools).map { |pool_number| @wrestler.weight.wrestlers_in_pool(pool_number).size }.max
wrestler_pool_size < largest_pool_size
end
def any_bye_round_had_wrestled_match?(bye_matches)
bye_matches.any? do |bye_match|
next false if bye_match.round.nil?
@wrestler.weight.matches.any? do |match|
next false if match.id == bye_match.id
next false if match.round != bye_match.round
next false if match.is_consolation_match != bye_match.is_consolation_match
match.finished == 1 && match.win_type.present? && match.win_type != "BYE"
end
end
end
end

View File

@@ -28,7 +28,7 @@ json.cache! ["api_tournament", @tournament] do
json.mats @mats do |mat|
json.name mat.name
json.unfinished_matches mat.unfinished_matches do |match|
json.unfinished_matches mat.queue_matches.compact do |match|
json.bout_number match.bout_number
json.w1_name match.w1_name
json.w2_name match.w2_name

View File

@@ -38,9 +38,10 @@
<li><strong>Pages</strong></li>
<li></span> <%= link_to "Edit Tournament Info", edit_tournament_path(@tournament) %></li>
<li><%= link_to "Weigh In Page" , "/tournaments/#{@tournament.id}/weigh_in" %></li>
<li><%= link_to "All Matches" , "/tournaments/#{@tournament.id}/matches" %></li>
<li><%= link_to "Full Screen Bout Board" , "/tournaments/#{@tournament.id}/up_matches?print=true" , target: :_blank %></li>
<li><%= link_to "Deduct Team Points" , "/tournaments/#{@tournament.id}/teampointadjust" %></li>
<li><%= link_to "All Matches" , "/tournaments/#{@tournament.id}/matches" %></li>
<li><%= link_to "Full Screen Bout Board" , "/tournaments/#{@tournament.id}/up_matches?print=true" , target: :_blank %></li>
<li><%= link_to "QR Code (Full Screen)" , "/tournaments/#{@tournament.id}/qrcode?print=true" , target: :_blank %></li>
<li><%= link_to "Deduct Team Points" , "/tournaments/#{@tournament.id}/teampointadjust" %></li>
<li><%= link_to "View All Mat Assignment Rules", tournament_mat_assignment_rules_path(@tournament) %></li>
<li><%= link_to 'Manage Backups', tournament_tournament_backups_path(@tournament) %></li>
<li><%= link_to "Reset Bout Board", reset_bout_board_tournament_path(@tournament), data: { turbo_method: :post, turbo_confirm: "Are you sure you want to reset the bout board?" } %></li>

View File

@@ -36,7 +36,11 @@
<div id="page-content">
<div class="row">
<div class="col-md-12"><%= render 'layouts/underheader' %></div>
<div class="col-md-12">
<% unless hide_ads? %>
<%= render 'layouts/underheader' %>
<% end %>
</div>
</div>
<div class="row no-margin">
<div class="col-md-12" style="padding-left: 2%;">
@@ -58,4 +62,3 @@
</body>
<% end %>
</html>

View File

@@ -18,8 +18,19 @@
<div id="cable-status-indicator" data-match-data-target="statusIndicator" class="alert alert-secondary" style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
<h4>Bout <strong><%= @match.bout_number %></strong></h4>
<% if @show_next_bout_button && @next_match %>
<%= link_to "Skip to Next Match for Mat #{@mat.name}", mat_path(@mat, bout_number: @next_match.bout_number), class: "btn btn-primary" %>
<% if @mat %>
<% queue_matches = @queue_matches || @mat.queue_matches %>
<div style="margin-bottom: 10px;">
<% queue_matches.each_with_index do |queue_match, index| %>
<% queue_label = "Queue #{index + 1}" %>
<% if queue_match %>
<% button_class = queue_match.id == @match.id ? "btn btn-success btn-sm" : "btn btn-primary btn-sm" %>
<%= link_to "#{queue_label}: Bout #{queue_match.bout_number}", mat_path(@mat, bout_number: queue_match.bout_number), class: button_class %>
<% else %>
<button type="button" class="btn btn-default btn-sm" disabled><%= "#{queue_label}: Not assigned" %></button>
<% end %>
<% end %>
</div>
<% end %>
<h4>Bracket Position: <strong><%= @match.bracket_position %></strong></h4>

View File

@@ -0,0 +1,34 @@
<h1>Assign Mat/Queue for Match <%= @match.bout_number %></h1>
<% if @current_mat %>
<p>Current Assignment: Mat <%= @current_mat.name %><%= @current_queue_position ? " (Queue #{@current_queue_position})" : "" %></p>
<% else %>
<p>Current Assignment: Unassigned</p>
<% end %>
<%= form_with model: @match, url: update_assignment_match_path(@match), method: :patch do |f| %>
<div class="field">
<%= f.label :mat_id, "Mat" %><br>
<%= f.collection_select :mat_id, @mats, :id, :name, { include_blank: "Unassigned" } %>
</div>
<br>
<div class="field">
<%= f.label :queue_position, "Queue Position" %><br>
<%= f.select :queue_position,
options_for_select(
[
["On Mat (Queue 1)", 1],
["On Deck (Queue 2)", 2],
["In The Hole (Queue 3)", 3],
["Warm Up (Queue 4)", 4]
],
@current_queue_position
),
include_blank: "Select position"
%>
</div>
<br>
<div class="actions">
<%= f.submit "Update Assignment", class: "btn btn-success" %>
</div>
<% end %>

View File

@@ -1,6 +1,8 @@
<% @mat = mat %>
<% @match = local_assigns[:match] || mat.unfinished_matches.first %>
<% @next_match = local_assigns[:next_match] || mat.unfinished_matches.second %>
<% @queue_matches = local_assigns[:queue_matches] || mat.queue_matches %>
<% @match = local_assigns[:match] || @queue_matches[0] %>
<% @match ||= @queue_matches[0] %>
<% @next_match = local_assigns[:next_match] || @queue_matches[1] %>
<% @show_next_bout_button = local_assigns.key?(:show_next_bout_button) ? local_assigns[:show_next_bout_button] : true %>
<% @wrestlers = [] %>

View File

@@ -0,0 +1,13 @@
<% if local_assigns[:school_permission_key].present? %>
<% wrestler_path_with_key = wrestler_path(wrestler) %>
<% wrestler_path_with_key += "?school_permission_key=#{school_permission_key}" %>
<td><%= link_to wrestler.name, wrestler_path_with_key %></td>
<% else %>
<td><%= link_to wrestler.name, wrestler_path(wrestler) %></td>
<% end %>
<td><%= wrestler.weight.max %></td>
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %> <%= wrestler.criteria %></td>
<td><%= wrestler.original_seed %></td>
<td><%= wrestler.total_team_points - wrestler.total_points_deducted %></td>
<td><%= "Yes" if wrestler.extra? %></td>
<td><%= wrestler.next_match_bout_number %> <%= wrestler.next_match_mat_name %></td>

View File

@@ -54,19 +54,8 @@
<tbody>
<% @wrestlers.sort_by { |w| w.weight.max }.each do |wrestler| %>
<% if params[:school_permission_key].present? %>
<!-- No caching when school_permission_key is present -->
<tr>
<td>
<% wrestler_path_with_key = wrestler_path(wrestler) %>
<% wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
<%= link_to wrestler.name, wrestler_path_with_key %>
</td>
<td><%= wrestler.weight.max %></td>
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %> <%= wrestler.criteria %></td>
<td><%= wrestler.original_seed %></td>
<td><%= wrestler.total_team_points - wrestler.total_points_deducted %></td>
<td><%= "Yes" if wrestler.extra? %></td>
<td><%= wrestler.next_match_bout_number %> <%= wrestler.next_match_mat_name %></td>
<%= render "schools/wrestler_row_cells", wrestler: wrestler, school_permission_key: params[:school_permission_key] %>
<% if can? :manage, wrestler.school %>
<td>
@@ -76,36 +65,31 @@
<% delete_wrestler_path_with_key = wrestler_path(wrestler) %>
<% delete_wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
<%= link_to '', edit_wrestler_path_with_key, class: "fas fa-edit" %>
<%= link_to '', delete_wrestler_path_with_key, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "fas fa-trash-alt" %>
<%= link_to edit_wrestler_path_with_key, class: "text-decoration-none" do %>
<span class="fas fa-edit" aria-hidden="true"></span>
<% end %>
<%= link_to delete_wrestler_path_with_key, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "text-decoration-none" do %>
<span class="fas fa-trash-alt" aria-hidden="true"></span>
<% end %>
</td>
<% end %>
</tr>
<% else %>
<!-- Use caching only when school_permission_key is NOT present -->
<% cache ["#{wrestler.id}_school_show", @school] do %>
<tr>
<td><%= link_to wrestler.name, wrestler_path(wrestler) %></td>
<td><%= wrestler.weight.max %></td>
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %> <%= wrestler.criteria %></td>
<td><%= wrestler.original_seed %></td>
<td><%= wrestler.total_team_points - wrestler.total_points_deducted %></td>
<td><%= "Yes" if wrestler.extra? %></td>
<td><%= wrestler.next_match_bout_number %> <%= wrestler.next_match_mat_name %></td>
<% end %>
<% if can? :manage, wrestler.school %>
<td>
<% edit_wrestler_path_with_key = edit_wrestler_path(wrestler) %>
<% edit_wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
<% delete_wrestler_path_with_key = wrestler_path(wrestler) %>
<% delete_wrestler_path_with_key += "?school_permission_key=#{params[:school_permission_key]}" if params[:school_permission_key].present? %>
<%= link_to '', edit_wrestler_path_with_key, class: "fas fa-edit" %>
<%= link_to '', delete_wrestler_path_with_key, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "fas fa-trash-alt" %>
</td>
<% end %>
</tr>
<tr>
<% cache ["school_show_wrestler_cells", wrestler] do %>
<%= render "schools/wrestler_row_cells", wrestler: wrestler %>
<% end %>
<% if can? :manage, wrestler.school %>
<td>
<%= link_to edit_wrestler_path(wrestler), class: "text-decoration-none" do %>
<span class="fas fa-edit" aria-hidden="true"></span>
<% end %>
<%= link_to wrestler_path(wrestler), data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? This will delete all of his matches." }, class: "text-decoration-none" do %>
<span class="fas fa-trash-alt" aria-hidden="true"></span>
<% end %>
</td>
<% end %>
</tr>
<% end %>
<% end %>
</tbody>

View File

@@ -48,7 +48,7 @@
<li>Win by major: 1pt extra</li>
<li>Win by tech fall: 1.5pt extra</li>
<li>Win by fall, default, dq: 2pt extra</li>
<li>BYE points: 2pt (if you win at least 1 match in a pool with a BYE)</li>
<li>BYE points: 2pt (if you win at least 1 match in a pool with a BYE). - This only applies if your pool has more BYEs than other pools in your bracket. This does not apply to weight classes with 1 pool.</li>
</ul>
<p>See placement points below (based on the largest bracket of the tournament)</p>
<h4>Pool Types</h4>
@@ -71,7 +71,7 @@
<li>Win by major: 1pt extra</li>
<li>Win by tech: 1.5pt extra</li>
<li>Win by fall, default, dq, etc: 2pt extra</li>
<li>BYE points: 2pts if you have a bye in the championship bracket and win the next match. 1pt if you have a bye in the consolation bracket and win the next match.</li>
<li>BYE points: 2pts if you have a bye in the championship bracket and win the next match. 1pt if you have a bye in the consolation bracket and win the next match. - This only applies if you received a bye in a round with at least 1 match in your backet.</li>
</ul>
<br>
<h3>Modified 16 Man Double Elimination Information</h3>
@@ -142,7 +142,7 @@
<br>
<h3>Future Plans</h3>
<br>
<p>Future development plans to support 32 and 64 man regulard double elimination, modified (5 per day match rule) 32 man double elimination, and true second double elimination brackets are underway.</p>
<p>Future development plans are underway to make the application more flexible, make changes after weigh ins easier, and to add functionality for a live scoreboard.</p>
<br>
<h3>Contact</h3>
<br>

View File

@@ -1,11 +1,5 @@
<% if @tournaments.size > 0 %>
<h3>My Tournaments</h3>
<script>
// $(document).ready(function() {
// $('#tournamentList').dataTable();
// pagingType: "bootstrap";
// } );
</script>
<table class="table table-hover" id="tournamentList">
<thead>
<tr>

View File

@@ -27,12 +27,12 @@ and will also delete all of your current data. It's best to use the create backu
</tbody>
</table>
<br><br>
<h3>Import Manual Backup</h3>
<p>Paste the backup text here. Note, if this is formatted wrong, you'll need to restore a backup from above to fix it and you'll see an error in your background jobs.</p>
<%= form_for(:tournament, url: import_manual_tournament_tournament_backups_path(@tournament)) do |f| %>
<div class="field">
<%= f.label 'Import text' %><br>
<%= f.text_area :import_text, cols: "30", rows: "20" %>
</div>
<%= submit_tag "Import", class: "btn btn-success", data: { turbo_confirm: 'Are you sure? This will delete everything for the current tournament and restore it with the backup text pasted below.' } %>
<% end %>
<% if ENV["RAILS_ENV"] == "development" %>
<%= form_for(:tournament, url: import_manual_tournament_tournament_backups_path(@tournament)) do |f| %>
<div class="field">
<%= f.label 'Import text' %><br>
<%= f.text_area :import_text, cols: "30", rows: "20" %>
</div>
<%= submit_tag "Import", class: "btn btn-success", data: { turbo_confirm: 'Are you sure? This will delete everything for the current tournament and restore it with the backup text pasted below.' } %>
<% end %>
<% end %>

View File

@@ -1,13 +1,15 @@
<% @final_match.each do |match| %>
<div class="round">
<div class="game">
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
<% if params[:print] %>
<div class="bout-number"><p><%= match.bout_number %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<% else %>
<div class="bout-number"><p><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<% end %>
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
</div>
</div>
<% cache ["bracket_final_match", match, match.wrestler1, match.wrestler2, @winner_place, params[:print].to_s] do %>
<div class="round">
<div class="game">
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
<% if params[:print] %>
<div class="bout-number"><p><%= match.bout_number %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<% else %>
<div class="bout-number"><p><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<% end %>
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
</div>
</div>
<% end %>
<% end %>

View File

@@ -1,18 +1,26 @@
<style>
table.smallText tr td { font-size: 10px; }
table.smallText {
border-collapse: collapse;
}
table.smallText th,
table.smallText td {
border: 1px solid #000;
}
/*
* Bracket Layout Specifics
*/
.bracket {
display: flex;
font-size: 10px;
font-size: 10.5px;
gap: 2px;
}
.game {
min-width: 150px;
min-height: 50px;
min-height: 58px;
/*background-color: #ddd;*/
border: 1px solid #000; /* Dark border so boxes stay visible when printed */
margin: 5px;
border: 1.5px solid #000; /* Dark border so boxes stay visible when printed */
margin: 4px;
}
/*.game:after {
@@ -56,14 +64,15 @@ table.smallText tr td { font-size: 10px; }
}
.game-top {
border-bottom:1px solid #000;
padding: 2px;
min-height: 12px;
border-bottom:1.5px solid #000;
padding: 3px 4px;
min-height: 16px;
}
.bout-number {
text-align: center;
/*padding-top: 15px;*/
line-height: 1.35;
padding: 1px 2px;
}
/* Style links within bout-number like default links */
@@ -77,15 +86,29 @@ table.smallText tr td { font-size: 10px; }
}
.bracket-winner {
border-bottom:1px solid #000;
padding: 2px;
min-height: 12px;
border-bottom:1.5px solid #000;
padding: 3px 4px;
min-height: 16px;
}
.game-bottom {
border-top:1px solid #000;
padding: 2px;
min-height: 12px;
border-top:1.5px solid #000;
padding: 3px 4px;
min-height: 16px;
}
@media print {
.game,
.game-top,
.game-bottom,
.bracket-winner,
table.smallText,
table.smallText th,
table.smallText td {
border-color: #000 !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
</style>
<% if @tournament.tournament_type == "Pool to bracket" %>
@@ -95,8 +118,6 @@ table.smallText tr td { font-size: 10px; }
<table class='smallText'>
<tr>
<td valign="top" style="padding: 10px;">
<% @matches = @tournament.matches.select{|m| m.weight_id == @weight.id} %>
<% @wrestlers = Wrestler.where(weight_id: @weight.id) %>
<% @pools = @weight.pool_rounds(@matches) %>
<%= render 'pool' %>
</td>

View File

@@ -1,13 +1,17 @@
<div class="round">
<% @round_matches.sort_by{|m| m.bracket_position_number}.each do |match| %>
<div class="game">
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
<% if params[:print] %>
<div class="bout-number"><%= match.bout_number %> <%= match.bracket_score_string %>&nbsp;</div>
<% else %>
<div class="bout-number"><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %>&nbsp;</div>
<% end %>
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
</div>
<% cache ["bracket_round_match", match, match.wrestler1, match.wrestler2, params[:print].to_s] do %>
<div class="game">
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
<% if params[:print] %>
<div class="bout-number"><%= match.bout_number %> <%= match.bracket_score_string %>&nbsp;</div>
<% else %>
<div class="bout-number"><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %>&nbsp;</div>
<% end %>
<div class="bout-number">Round <%= match.round %></div>
<div class="bout-number"><%= match.bracket_position %></div>
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
</div>
<% end %>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<% cache ["team_score_row", school, rank] do %>
<tr>
<td><%= rank %>. <%= school.name %> (<%= school.abbreviation %>)</td>
<td><%= school.page_score_string %></td>
</tr>
<% end %>

View File

@@ -0,0 +1,38 @@
<div id="up_matches_board">
<h3>Upcoming Matches</h3>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th>Mat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
<th>On Mat</th>
<th>On Deck</th>
<th>In The Hole</th>
<th>Warm Up</th>
</tr>
</thead>
<tbody>
<% (local_assigns[:mats] || tournament.up_matches_mats).each do |m| %>
<%= render "tournaments/up_matches_mat_row", mat: m %>
<% end %>
</tbody>
</table>
<br>
<h3>Matches not assigned</h3>
<br>
<table class="table table-striped table-bordered table-condensed" id="matchList">
<thead>
<tr>
<th>Round</th>
<th>Bout Number</th>
<th>Weight Class</th>
<th>Matchup</th>
</tr>
</thead>
<tbody>
<%= render partial: "tournaments/up_matches_unassigned_row", collection: (local_assigns[:matches] || tournament.up_matches_unassigned_matches), as: :match %>
</tbody>
</table>
<br>
</div>

View File

@@ -0,0 +1,35 @@
<% queue1_match, queue2_match, queue3_match, queue4_match = mat.queue_matches %>
<% queue_match_dependencies = [queue1_match, queue2_match, queue3_match, queue4_match].compact.flat_map { |match| [match, match.wrestler1, match.wrestler2] } %>
<% cache ["up_matches_mat_row", mat, *queue_match_dependencies] do %>
<tr>
<td><%= mat.name %></td>
<td>
<% if queue1_match %><strong><%= queue1_match.bout_number %></strong> (<%= queue1_match.bracket_position %>)<br>
<%= queue1_match.weight_max %> lbs
<br><%= queue1_match.w1_bracket_name %> vs. <br>
<%= queue1_match.w2_bracket_name %>
<% end %>
</td>
<td>
<% if queue2_match %><strong><%= queue2_match.bout_number %></strong> (<%= queue2_match.bracket_position %>)<br>
<%= queue2_match.weight_max %> lbs
<br><%= queue2_match.w1_bracket_name %> vs. <br>
<%= queue2_match.w2_bracket_name %>
<% end %>
</td>
<td>
<% if queue3_match %><strong><%= queue3_match.bout_number %></strong> (<%= queue3_match.bracket_position %>)<br>
<%= queue3_match.weight_max %> lbs
<br><%= queue3_match.w1_bracket_name %> vs. <br>
<%= queue3_match.w2_bracket_name %>
<% end %>
</td>
<td>
<% if queue4_match %><strong><%= queue4_match.bout_number %></strong> (<%= queue4_match.bracket_position %>)<br>
<%= queue4_match.weight_max %> lbs
<br><%= queue4_match.w1_bracket_name %> vs. <br>
<%= queue4_match.w2_bracket_name %>
<% end %>
</td>
</tr>
<% end %>

View File

@@ -0,0 +1,8 @@
<% cache ["up_matches_unassigned_row", match, match.wrestler1, match.wrestler2] do %>
<tr>
<td>Round <%= match.round %></td>
<td><%= match.bout_number %></td>
<td><%= match.weight_max %></td>
<td><%= match.w1_bracket_name %> vs. <%= match.w2_bracket_name %></td>
</tr>
<% end %>

View File

@@ -1,20 +1,20 @@
<style>
/* General styles for pages */
@page {
margin: 0.5in; /* Universal margin for all pages */
margin: 0.35in;
}
.page {
width: 7.5in; /* Portrait width (8.5in - margins) */
height: 10in; /* Portrait height (11in - margins) */
width: 7.8in; /* 8.5in - 2 * 0.35in */
height: 10.3in; /* 11in - 2 * 0.35in */
margin: auto;
overflow: hidden;
position: relative;
}
.page-landscape {
width: 10in; /* Landscape width (11in - margins) */
height: 7.5in; /* Landscape height (8.5in - margins) */
width: 10.3in; /* 11in - 2 * 0.35in */
height: 7.8in; /* 8.5in - 2 * 0.35in */
margin: auto;
overflow: hidden;
position: relative;
@@ -26,6 +26,11 @@
transform-origin: top left;
}
.bracket-container h4 {
margin-top: 0.15rem;
margin-bottom: 0.45rem;
}
/* Print-specific styles */
@media print {
/* Set orientation for portrait pages */
@@ -51,6 +56,10 @@
transform-origin: top left;
}
.bracket {
page-break-inside: avoid;
}
/* Optional: Hide elements not needed in print */
.no-print {
display: none;
@@ -62,15 +71,10 @@
function scaleContent() {
document.querySelectorAll('.page, .page-landscape').forEach(page => {
const container = page.querySelector('.bracket-container');
const isLandscape = page.classList.contains('page-landscape');
// Page dimensions (1 inch = 96px)
const pageWidth = isLandscape ? 10 * 96 : 7.5 * 96;
const pageHeight = isLandscape ? 7.5 * 96 : 10 * 96;
// Subtract margins (0.5 inch margin)
const availableWidth = pageWidth - (0.5 * 96 * 2);
const availableHeight = pageHeight - (0.5 * 96 * 2);
// Use the actual page box size (already accounts for @page margins)
const availableWidth = page.clientWidth;
const availableHeight = page.clientHeight;
// Measure content dimensions
const contentWidth = container.scrollWidth;
@@ -80,8 +84,8 @@
const scaleX = availableWidth / contentWidth;
const scaleY = availableHeight / contentHeight;
// Use a slightly relaxed scaling to avoid over-aggressive shrinking
const scale = Math.min(scaleX, scaleY, 1); // Ensure scale does not exceed 100% (1)
// Keep a tiny buffer so borders/text don't clip at print edges
const scale = Math.min(scaleX, scaleY, 1) * 0.99;
// Apply the scale
container.style.transform = `scale(${scale})`;
@@ -91,9 +95,9 @@
const scaledWidth = contentWidth * scale;
const scaledHeight = contentHeight * scale;
// Center the content within the page
const horizontalPadding = (pageWidth - scaledWidth) / 2;
const verticalPadding = (pageHeight - scaledHeight) / 2;
// Center the content within the available page box
const horizontalPadding = (availableWidth - scaledWidth) / 2;
const verticalPadding = (availableHeight - scaledHeight) / 2;
// Apply margin adjustments
container.style.marginLeft = `${Math.max(0, horizontalPadding)}px`;
@@ -119,16 +123,17 @@
<% @weights.sort_by{|w| w.max}.each do |weight| %>
<% if @tournament.tournament_type == "Pool to bracket" %>
<!-- Need to define what the tournaments#bracket controller defines -->
<% @matches = @tournament.matches.select{|m| m.weight_id == weight.id} %>
<% @wrestlers = Wrestler.where(weight_id: weight.id) %>
<% @matches = @matches_by_weight_id[weight.id] || [] %>
<% @wrestlers = @wrestlers_by_weight_id[weight.id] || [] %>
<% @pools = weight.pool_rounds(@matches) %>
<% @weight = weight %>
<%= render 'bracket_partial' %>
<% elsif @tournament.tournament_type.include? "Modified 16 Man Double Elimination" or @tournament.tournament_type.include? "Regular Double Elimination" %>
<!-- Need to define what the tournaments#bracket controller defines -->
<% @matches = weight.matches %>
<% @matches = @matches_by_weight_id[weight.id] || [] %>
<% @wrestlers = @wrestlers_by_weight_id[weight.id] || [] %>
<% @weight = weight %>
<%= render 'bracket_partial' %>
<% end %>
<% end %>
<% end %>
<% end %>

View File

@@ -54,28 +54,28 @@
}
</style>
<% @matches.each do |match| %>
<% if match.w1 && match.w2 %>
<% w1 = Wrestler.find(match.w1) %>
<% w2 = Wrestler.find(match.w2) %>
<% end %>
<div class="pagebreak">
<p><strong>Bout Number:</strong> <%= match.bout_number %> <strong>Weight Class:</strong> <%= match.weight.max %> <strong>Round:</strong> <%= match.round %> <strong>Bracket Position:</strong> <%= match.bracket_position %></p>
<p><strong>Key: </strong>Takedown: T3, Escape: E1, Reversal: R2, Nearfall: N2 or N3 or N4, Stalling: S, Caution: C, Penalty Point: P1</p>
<% @matches.each do |match| %>
<% w1 = @wrestlers_by_id[match.w1] %>
<% w2 = @wrestlers_by_id[match.w2] %>
<% w1_name = w1&.name || match.loser1_name %>
<% w2_name = w2&.name || match.loser2_name %>
<div class="pagebreak">
<p><strong>Bout Number:</strong> <%= match.bout_number %> <strong>Weight Class:</strong> <%= match.weight.max %> <strong>Round:</strong> <%= match.round %> <strong>Bracket Position:</strong> <%= match.bracket_position %></p>
<p><strong>Key: </strong>Takedown: T3, Escape: E1, Reversal: R2, Nearfall: N2 or N3 or N4, Stalling: S, Caution: C, Penalty Point: P1</p>
<table>
<thead>
<tr class="small-row">
<th class="fixed-width">Circle Winner</th>
<th>
<p><%= match.w1_name %>-<%= w1&.school&.name %></p>
</th>
<th>
<p><%= match.w2_name %>-<%= w2&.school&.name %></p>
</th>
</tr>
</thead>
<th class="fixed-width">Circle Winner</th>
<th>
<p><%= w1_name %>-<%= w1&.school&.name %></p>
</th>
<th>
<p><%= w2_name %>-<%= w2&.school&.name %></p>
</th>
</tr>
</thead>
<tbody>
<tr class="small-row">
<td class="fixed-width"></td>

View File

@@ -1,8 +1,8 @@
<% cache ["#{@weight.id}_bracket", @weight] do %>
<% cache ["#{@weight.id}_bracket", @weight, params[:print].to_s] do %>
<%= render 'bracket_partial' %>
<% end %>
<% if @tournament.tournament_type == "Pool to bracket" %>
<%= render 'pool_bracket_director_actions' %>
<% elsif @tournament.tournament_type.include? "Modified 16 Man Double Elimination" or @tournament.tournament_type.include? "Regular Double Elimination" %>
<%= render 'bracket_director_actions' %>
<% end %>
<% end %>

View File

@@ -1,23 +1,33 @@
{
"tournament": {
"attributes": <%= @tournament.attributes.to_json %>,
<%
wrestlers_by_id = @tournament.wrestlers.index_by(&:id)
weights_by_id = @tournament.weights.index_by(&:id)
mats_by_id = @tournament.mats.index_by(&:id)
sorted_matches = @tournament.matches.sort_by(&:bout_number)
%>
{
"tournament": {
"attributes": <%= @tournament.attributes.to_json %>,
"schools": <%= @tournament.schools.map(&:attributes).to_json %>,
"weights": <%= @tournament.weights.map(&:attributes).to_json %>,
"mats": <%= @tournament.mats.map(&:attributes).to_json %>,
"mats": <%= @tournament.mats.map { |mat| mat.attributes.merge(
{
"queue_bout_numbers": mat.queue_matches.map { |match| match&.bout_number }
}
) }.to_json %>,
"wrestlers": <%= @tournament.wrestlers.map { |wrestler| wrestler.attributes.merge(
{
"school": wrestler.school&.attributes,
"weight": wrestler.weight&.attributes
}
) }.to_json %>,
"matches": <%= @tournament.matches.sort_by(&:bout_number).map { |match| match.attributes.merge(
{
"w1_name": Wrestler.find_by(id: match.w1)&.name,
"w2_name": Wrestler.find_by(id: match.w2)&.name,
"winner_name": Wrestler.find_by(id: match.winner_id)&.name,
"weight": Weight.find_by(id: match.weight_id)&.attributes,
"mat": Mat.find_by(id: match.mat_id)&.attributes
}
) }.to_json %>
}
}
"matches": <%= sorted_matches.map { |match| match.attributes.merge(
{
"w1_name": wrestlers_by_id[match.w1]&.name,
"w2_name": wrestlers_by_id[match.w2]&.name,
"winner_name": wrestlers_by_id[match.winner_id]&.name,
"weight": weights_by_id[match.weight_id]&.attributes,
"mat": mats_by_id[match.mat_id]&.attributes
}
) }.to_json %>
}
}

View File

@@ -1,13 +1,15 @@
<h1>All <%= @tournament.name %> matches</h1>
<script>
$(document).ready(function() {
$('#matchesList').dataTable();
pagingType: "bootstrap";
} );
</script>
</br>
<% matches_path = "/tournaments/#{@tournament.id}/matches" %>
<%= form_tag(matches_path, method: :get, id: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: "Search wrestler, school, or bout #" %>
<%= submit_tag "Search" %>
<% end %>
<p>Search by wrestler name, school name, or bout number.</p>
<br>
<table class="table table-striped table-bordered table-condensed" id="matchesList">
<thead>
<tr>
@@ -28,12 +30,56 @@
<td><%= match.finished %></td>
<td><%= link_to 'Show', match, :class=>"btn btn-default btn-sm" %>
<%= link_to 'Edit Wrestlers', edit_match_path(match), :class=>"btn btn-primary btn-sm" %>
<%= link_to 'Edit Mat/Queue', edit_assignment_match_path(match), :class=>"btn btn-primary btn-sm" %>
<%= link_to 'Stat Match', "/matches/#{match.id}/stat", :class=>"btn btn-primary btn-sm" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% if @total_pages.present? && @total_pages > 1 %>
<nav aria-label="Matches pagination">
<ul class="pagination">
<% if @page > 1 %>
<li class="page-item">
<%= link_to "Previous", { controller: "tournaments", action: "matches", id: @tournament.id, page: @page - 1, search: params[:search] }, class: "page-link" %>
</li>
<% else %>
<li class="page-item disabled"><span class="page-link">Previous</span></li>
<% end %>
<% window = 5
left = [1, @page - window / 2].max
right = [@total_pages, left + window - 1].min
left = [1, right - window + 1].max
%>
<% (left..right).each do |p| %>
<% if p == @page %>
<li class="page-item active"><span class="page-link"><%= p %></span></li>
<% else %>
<li class="page-item"><%= link_to p, { controller: "tournaments", action: "matches", id: @tournament.id, page: p, search: params[:search] }, class: "page-link" %></li>
<% end %>
<% end %>
<% if @page < @total_pages %>
<li class="page-item">
<%= link_to "Next", { controller: "tournaments", action: "matches", id: @tournament.id, page: @page + 1, search: params[:search] }, class: "page-link" %>
</li>
<% else %>
<li class="page-item disabled"><span class="page-link">Next</span></li>
<% end %>
</ul>
</nav>
<p class="text-muted">
<% start_index = ((@page - 1) * @per_page) + 1
end_index = [@page * @per_page, @total_count].min
%>
Showing <%= start_index %> - <%= end_index %> of <%= @total_count %> matches
</p>
<% end %>
<br>
<p>Total matches without byes: <%= @matches.select{|m| m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %></p>
<p>Unfinished matches: <%= @matches.select{|m| m.finished != 1 and m.loser1_name != 'BYE' and m.loser2_name != 'BYE'}.size %></p>
<p>Total matches without byes: <%= @matches_without_byes_count %></p>
<p>Unfinished matches: <%= @unfinished_matches_without_byes_count %></p>

View File

@@ -0,0 +1,50 @@
<style>
.qr-page {
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 24px;
}
.qr-page h1 {
margin: 0 0 24px 0;
font-size: 40px;
font-weight: 700;
}
.qr-code {
width: 100%;
display: flex;
justify-content: center;
}
.qr-code svg {
width: min(80vmin, 720px);
height: auto;
display: block;
margin: 0;
}
@media print {
body {
margin: 0;
}
}
</style>
<div class="qr-page">
<h1><%= @tournament.name %> Brackets and Results Available Here</h1>
<div class="qr-code">
<%= raw @qrcode.as_svg(
offset: 0,
color: "000",
shape_rendering: "crispEdges",
module_size: 8,
standalone: true
) %>
</div>
</div>

View File

@@ -70,9 +70,13 @@
</td>
<td>
<% if can? :manage, school %>
<%= link_to '', edit_school_path(school), :class=>"fas fa-edit" %>
<%= link_to edit_school_path(school), class: "text-decoration-none" do %>
<span class="fas fa-edit" aria-hidden="true"></span>
<% end %>
<% if can? :manage, @tournament %>
<%= link_to '', school, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{school.name}?" }, :class=>"fas fa-trash-alt" %>
<%= link_to school, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{school.name}?" }, class: "text-decoration-none" do %>
<span class="fas fa-trash-alt" aria-hidden="true"></span>
<% end %>
<% end %>
<% end %>
</td>
@@ -105,8 +109,12 @@
<td><%= weight.bracket_size %></td>
<% if can? :manage, @tournament %>
<td>
<%= link_to '', edit_weight_path(weight), :class=>"fas fa-edit" %>
<%= link_to '', weight, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete the #{weight.max} weight class?" }, :class=>"fas fa-trash-alt" %>
<%= link_to edit_weight_path(weight), class: "text-decoration-none" do %>
<span class="fas fa-edit" aria-hidden="true"></span>
<% end %>
<%= link_to weight, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete the #{weight.max} weight class?" }, class: "text-decoration-none" do %>
<span class="fas fa-trash-alt" aria-hidden="true"></span>
<% end %>
</td>
<% end %>
</tr>
@@ -130,7 +138,9 @@
<td><%= link_to "Mat #{mat.name}", mat %></td>
<% if can? :manage, @tournament %>
<td>
<%= link_to '', mat, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete Mat #{mat.name}?" }, :class=>"fas fa-trash-alt" %>
<%= link_to mat, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete Mat #{mat.name}?" }, class: "text-decoration-none" do %>
<span class="fas fa-trash-alt" aria-hidden="true"></span>
<% end %>
<%= link_to '', "/mats/#{mat.id}/assign_next_match", data: { turbo_method: :post }, :class=>"fas fa-solid fa-arrow-right" %>
</td>
<% end %>

View File

@@ -1,6 +1,5 @@
<% cache ["#{@tournament.id}_team_scores", @tournament] do %>
<% team_scores_last_updated = @schools.map(&:updated_at).compact.max&.utc&.to_fs(:nsec) %>
<% cache ["team_scores", @tournament.id, @schools.size, team_scores_last_updated] do %>
<table class="pagebreak table table-striped table-bordered">
<h3>Team Scores</h3>
<thead>
@@ -11,12 +10,9 @@
</thead>
<tbody>
<% @schools.each do |school| %>
<tr>
<td><%= @schools.index(school) + 1 %>. <%= school.name %> (<%= school.abbreviation %>)</td>
<td><%= school.page_score_string %></td>
</tr>
<% @schools.each_with_index do |school, index| %>
<%= render "tournaments/team_score_row", school: school, rank: index + 1 %>
<% end %>
</tbody>
</table>
<% end %>
<% end %>

View File

@@ -1,67 +1,19 @@
<% cache ["#{@tournament.id}_up_matches", @tournament] do %>
<script>
// $(document).ready(function() {
// $('#matchList').dataTable();
// } );
</script>
<script>
setTimeout("location.reload(true);",30000);
</script>
<br>
<br>
<h5 style="color:red">This page reloads every 30s</h5>
<br>
<h3>Upcoming Matches</h3>
<br>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th>Mat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
<th>On Mat</th>
<th>On Deck</th>
<th>In The Hole</th>
<th>Warm Up</th>
</tr>
</thead>
<div data-controller="up-matches-connection">
<% if params[:print] != "true" %>
<div style="margin-bottom: 10px;">
<%= link_to "Show Bout Board in Full Screen", up_matches_path(@tournament, print: true), class: "btn btn-primary" %>
</div>
<% end %>
<tbody>
<% @mats.each.map do |m| %>
<tr>
<td><%= m.name %></td>
<td><% if m.unfinished_matches.first %><strong><%=m.unfinished_matches.first.bout_number%></strong> - <%= m.unfinished_matches.first.weight_max %><br><%= m.unfinished_matches.first.w1_bracket_name %> vs. <%= m.unfinished_matches.first.w2_bracket_name %><% end %></td>
<td><% if m.unfinished_matches.second %><strong><%=m.unfinished_matches.second.bout_number%></strong> - <%= m.unfinished_matches.second.weight_max %><br><%= m.unfinished_matches.second.w1_bracket_name %> vs. <%= m.unfinished_matches.second.w2_bracket_name %><% end %></td>
<td><% if m.unfinished_matches.third %><strong><%=m.unfinished_matches.third.bout_number%></strong> - <%= m.unfinished_matches.third.weight_max %><br><%= m.unfinished_matches.third.w1_bracket_name %> vs. <%= m.unfinished_matches.third.w2_bracket_name %><% end %></td>
<td><% if m.unfinished_matches.fourth %><strong><%=m.unfinished_matches.fourth.bout_number%></strong> - <%= m.unfinished_matches.fourth.weight_max %><br><%= m.unfinished_matches.fourth.w1_bracket_name %> vs. <%= m.unfinished_matches.fourth.w2_bracket_name %><% end %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<h3>Matches not assigned</h3>
<br>
<table class="table table-striped table-bordered table-condensed" id="matchList">
<thead>
<tr>
<th>Round</th>
<th>Bout Number</th>
<th>Weight Class</th>
<th>Matchup</th>
</tr>
</thead>
<%= turbo_stream_from @tournament, data: { up_matches_connection_target: "stream" } %>
<div
id="up-matches-cable-status-indicator"
data-up-matches-connection-target="statusIndicator"
class="alert alert-secondary"
style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"
>
Connecting to server for real-time up matches updates...
</div>
<tbody>
<% if @matches.size > 0 %>
<% @matches.each.map do |m| %>
<tr>
<td>Round <%= m.round %></td>
<td><%= m.bout_number %></td>
<td><%= m.weight_max %></td>
<td><%= m.w1_bracket_name %> vs. <%= m.w2_bracket_name %></td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<br>
<% end %>
<%= render "up_matches_board", tournament: @tournament, mats: @mats, matches: @matches %>
</div>

View File

@@ -12,9 +12,10 @@
height: 1in;
}
</style>
<% @tournament.schools.each do |school| %>
<% @schools.each do |school| %>
<table class="table table-striped table-bordered table-condensed pagebreak">
<h5><%= school.name %></h4>
<p><strong>Weigh In Ref:</strong> <%= @tournament.weigh_in_ref %></p>
<thead>
<tr>
<th>Name</th>
@@ -27,9 +28,9 @@
<tr>
<td><%= wrestler.name %></td>
<td><%= wrestler.weight.max %></td>
<td></td>
<td><%= wrestler.offical_weight %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<% end %>

View File

@@ -1,4 +1,4 @@
<table class="table table-striped table-bordered">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Name</th>
@@ -9,19 +9,19 @@
</tr>
</thead>
<tbody>
<%= form_tag @wrestlers_update_path do %>
<% @wrestlers.order("original_seed asc").each do |wrestler| %>
<% if wrestler.weight_id == @weight.id %>
<tr>
<%= form_tag "/tournaments/#{@tournament.id}/weigh_in/#{@weight.id}", method: :post do %>
<% @wrestlers.order("original_seed asc").each do |wrestler| %>
<% if wrestler.weight_id == @weight.id %>
<tr>
<td><%= wrestler.name %></td>
<td><%= wrestler.school.name %></td>
<td><%= wrestler.original_seed %></td>
<td><%= wrestler.weight.max %></td>
<td>
<% if user_signed_in? %>
<%= fields_for "wrestler[]", wrestler do |w| %>
<%= w.number_field :offical_weight, :step => 'any' %>
<% end %>
<% if user_signed_in? %>
<%= fields_for "wrestler[#{wrestler.id}]", wrestler do |w| %>
<%= w.number_field :offical_weight, :step => 'any' %>
<% end %>
<% else %>
<%= wrestler.offical_weight %>
<% end %>

View File

@@ -0,0 +1,10 @@
<% cache ["weight_show_wrestler_row", wrestler] do %>
<tr>
<td><%= link_to wrestler.name, wrestler %></td>
<td><%= wrestler.school.name %></td>
<td><%= wrestler.original_seed %></td>
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %></td>
<td><%= wrestler.criteria %> Win <%= wrestler.season_win_percentage %>%</td>
<td><%= "Yes" if wrestler.extra? %></td>
</tr>
<% end %>

View File

@@ -1,48 +1,51 @@
<h3>Weight Class:<%= @weight.max %> <% if can? :manage, @tournament %><%= link_to " Edit", edit_weight_path(@weight), :class=>"fas fa-edit" %><% end %></h3>
<h3>Weight Class:<%= @weight.max %> <% if can? :manage, @tournament %><%= link_to edit_weight_path(@weight), class: "text-decoration-none" do %><span class="fas fa-edit" aria-hidden="true"></span><% end %><% end %></h3>
<br>
<br>
<table class="table table-hover table-condensed">
<thead>
<tr>
<th>Name</th>
<th>School</th>
<th>Seed</th>
<th>Record</th>
<th>Seed Criteria</th>
<th>Extra?</th>
</tr>
</thead>
<tbody>
<%= form_tag @wrestlers_update_path do %>
<% @wrestlers.sort_by{|w| [w.original_seed ? 0 : 1, w.original_seed || 0]}.each do |wrestler| %>
<% if wrestler.weight_id == @weight.id %>
<tr>
<td><%= link_to "#{wrestler.name}", wrestler %></td>
<td><%= wrestler.school.name %></td>
<td>
<% if can? :manage, @tournament %>
<%= fields_for "wrestler[]", wrestler do |w| %>
<%= w.text_field :original_seed %>
<% end %>
<% else %>
<%= wrestler.original_seed %>
<%= form_tag @wrestlers_update_path do %>
<table class="table table-hover table-condensed">
<thead>
<tr>
<th>Name</th>
<th>School</th>
<th>Seed</th>
<th>Record</th>
<th>Seed Criteria</th>
<th>Extra?</th>
</tr>
</thead>
<tbody>
<% sorted_wrestlers = @wrestlers.sort_by{|w| [w.original_seed ? 0 : 1, w.original_seed || 0]} %>
<% if can? :manage, @tournament %>
<% sorted_wrestlers.each do |wrestler| %>
<% if wrestler.weight_id == @weight.id %>
<tr>
<td><%= link_to wrestler.name, wrestler %></td>
<td><%= wrestler.school.name %></td>
<td>
<%= fields_for "wrestler[]", wrestler do |w| %>
<%= w.text_field :original_seed %>
<% end %>
</td>
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %></td>
<td><%= wrestler.criteria %> Win <%= wrestler.season_win_percentage %>%</td>
<td><% if wrestler.extra? == true %>
Yes
<% end %></td>
<% if can? :manage, @tournament %>
<td>
<%= link_to '', wrestler, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? THIS WILL DELETE ALL MATCHES." } , :class=>"fas fa-trash-alt" %>
</td>
<% end %>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<td><%= wrestler.season_win %>-<%= wrestler.season_loss %></td>
<td><%= wrestler.criteria %> Win <%= wrestler.season_win_percentage %>%</td>
<td><%= "Yes" if wrestler.extra? %></td>
<td>
<%= link_to wrestler, data: { turbo_method: :delete, turbo_confirm: "Are you sure you want to delete #{wrestler.name}? THIS WILL DELETE ALL MATCHES." }, class: "text-decoration-none" do %>
<span class="fas fa-trash-alt" aria-hidden="true"></span>
<% end %>
</td>
</tr>
<% end %>
<% end %>
<% else %>
<% sorted_wrestlers.each do |wrestler| %>
<% if wrestler.weight_id == @weight.id %>
<%= render "weights/readonly_wrestler_row", wrestler: wrestler %>
<% end %>
<% end %>
<% end %>
</tbody>
</table>
<br><p>*All wrestlers without a seed (determined by tournament director) will be assigned a random bracket line.</p>
<% if can? :manage, @tournament %>
<br>

6
bin/bundler-audit Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "bundler/audit/cli"
ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check")
Bundler::Audit::CLI.start

6
bin/ci Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "active_support/continuous_integration"
CI = ActiveSupport::ContinuousIntegration
require_relative "../config/ci.rb"

View File

@@ -2,7 +2,7 @@
require "rubygems"
require "bundler/setup"
# explicit rubocop config increases performance slightly while avoiding config confusion.
# Explicit RuboCop config increases performance slightly while avoiding config confusion.
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
load Gem.bin_path("rubocop", "rubocop")

View File

@@ -4,5 +4,5 @@ project_dir="$(dirname $( dirname $(readlink -f ${BASH_SOURCE[0]})))"
cd ${project_dir}
bundle exec rake db:migrate RAILS_ENV=test
CI=true brakeman
bundle exec bundle-audit check --update
bundle exec rake test
bundle audit
rails test -v

View File

@@ -22,6 +22,7 @@ FileUtils.chdir APP_ROOT do
puts "\n== Preparing database =="
system! "bin/rails db:prepare"
system! "bin/rails db:reset" if ARGV.include?("--reset")
puts "\n== Removing old logs and tempfiles =="
system! "bin/rails log:clear tmp:clear"

View File

@@ -25,9 +25,6 @@ module Wrestling
config.active_record.schema_format = :ruby
config.active_record.dump_schemas = :all
# Fix deprecation warning for to_time in Rails 8.1
config.active_support.to_time_preserves_timezone = :zone
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
@@ -56,6 +53,6 @@ module Wrestling
# Valid values are 7.0 or 7.1
config.active_support.cache_format_version = 7.1
config.load_defaults 8.0
config.load_defaults 8.1
end
end

5
config/bundler-audit.yml Normal file
View File

@@ -0,0 +1,5 @@
# Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit.
# CVEs that are not relevant to the application can be enumerated on the ignore list below.
ignore:
- CVE-THAT-DOES-NOT-APPLY

View File

@@ -1,9 +1,10 @@
# Async adapter only works within the same process, so for manually triggering cable updates from a console,
# and seeing results in the browser, you must do so from the web console (running inside the dev process),
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
# to make the web console appear.
development:
adapter: async
adapter: solid_cable
connects_to:
database:
writing: cable
polling_interval: 0.1.seconds
message_retention: 1.day
test:
adapter: test

24
config/ci.rb Normal file
View File

@@ -0,0 +1,24 @@
# Run using bin/ci
CI.run do
step "Setup", "bin/setup --skip-server"
step "Style: Ruby", "bin/rubocop"
step "Security: Gem audit", "bin/bundler-audit"
step "Security: Importmap vulnerability audit", "bin/importmap audit"
step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
step "Tests: Rails", "bin/rails test"
step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"
# Optional: Run system tests
# step "Tests: System", "bin/rails test:system"
# Optional: set a green GitHub commit status to unblock PR merge.
# Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`.
# if success?
# step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
# else
# failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
# end
end

View File

@@ -11,9 +11,8 @@ pin "@rails/actioncable", to: "actioncable.esm.js" # For Action Cable
# and pin it directly, e.g., pin "jquery", to: "jquery.min.js"
pin "jquery", to: "jquery.js"
# Pin Bootstrap and DataTables from vendor/assets/javascripts/
# Pin Bootstrap from vendor/assets/javascripts/
pin "bootstrap", to: "bootstrap.min.js"
pin "datatables.net", to: "jquery.dataTables.min.js" # Assuming this is how you want to import it
# If Bootstrap requires Popper.js, and you have it in vendor/assets/javascripts/
# pin "@popperjs/core", to: "popper.min.js" # Or the actual filename if different
@@ -22,4 +21,4 @@ pin "datatables.net", to: "jquery.dataTables.min.js" # Assuming this is how you
pin_all_from "app/assets/javascripts/controllers", under: "controllers"
# Pin all JS files from app/assets/javascripts directory
pin_all_from "app/assets/javascripts", under: "assets/javascripts"
pin_all_from "app/assets/javascripts", under: "assets/javascripts"

View File

@@ -10,6 +10,8 @@ Wrestling::Application.routes.draw do
member do
get :stat
get :spectate
get :edit_assignment
patch :update_assignment
end
end
@@ -71,6 +73,7 @@ Wrestling::Application.routes.draw do
get 'tournaments/:id/bout_sheets' => 'tournaments#bout_sheets'
get 'tournaments/:id/no_matches' => 'tournaments#no_matches'
get 'tournaments/:id/matches' => 'tournaments#matches'
get 'tournaments/:id/qrcode' => 'tournaments#qrcode'
get 'tournaments/:id/delegate' => 'tournaments#delegate', :as => :tournament_delegate
post 'tournaments/:id/delegate' => 'tournaments#delegate', :as => :set_tournament_delegate
delete 'tournaments/:id/:delegate/remove_delegate' => 'tournaments#remove_delegate', :as => :delete_delegate_path

View File

@@ -0,0 +1,49 @@
class AddQueuesToMats < ActiveRecord::Migration[7.0]
class Mat < ActiveRecord::Base
self.table_name = "mats"
has_many :matches, class_name: "AddQueuesToMats::Match", foreign_key: "mat_id"
end
class Match < ActiveRecord::Base
self.table_name = "matches"
end
def up
add_column :mats, :queue1, :bigint
add_column :mats, :queue2, :bigint
add_column :mats, :queue3, :bigint
add_column :mats, :queue4, :bigint
add_index :mats, :queue1
add_index :mats, :queue2
add_index :mats, :queue3
add_index :mats, :queue4
say_with_time "Backfilling mat queues from unfinished matches" do
Mat.reset_column_information
Match.reset_column_information
Mat.find_each do |mat|
match_ids = mat.matches.where(finished: [nil, 0]).order(:bout_number).limit(4).pluck(:id)
mat.update_columns(
queue1: match_ids[0],
queue2: match_ids[1],
queue3: match_ids[2],
queue4: match_ids[3]
)
end
end
end
def down
remove_index :mats, :queue1
remove_index :mats, :queue2
remove_index :mats, :queue3
remove_index :mats, :queue4
remove_column :mats, :queue1
remove_column :mats, :queue2
remove_column :mats, :queue3
remove_column :mats, :queue4
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_04_15_173921) do
ActiveRecord::Schema[8.0].define(version: 2026_01_29_120000) do
create_table "mat_assignment_rules", force: :cascade do |t|
t.bigint "tournament_id"
t.bigint "mat_id"
@@ -56,6 +56,14 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_15_173921) do
t.bigint "tournament_id"
t.datetime "created_at", precision: nil
t.datetime "updated_at", precision: nil
t.bigint "queue1"
t.bigint "queue2"
t.bigint "queue3"
t.bigint "queue4"
t.index ["queue1"], name: "index_mats_on_queue1"
t.index ["queue2"], name: "index_mats_on_queue2"
t.index ["queue3"], name: "index_mats_on_queue3"
t.index ["queue4"], name: "index_mats_on_queue4"
t.index ["tournament_id"], name: "index_mats_on_tournament_id"
end

View File

@@ -1,4 +1,4 @@
FROM ruby:3.2.0
FROM ruby:4.0.1
# Accept build arguments for user/group IDs
ARG USER_ID=1000

View File

@@ -1,4 +1,4 @@
FROM ruby:3.2.0-slim
FROM ruby:4.0.1-slim
#HEALTHCHECK --start-period=60s CMD curl http://127.0.0.1/
@@ -15,6 +15,8 @@ RUN apt-get -qq update --fix-missing \
libsqlite3-dev \
wget \
default-libmysqlclient-dev \
libyaml-dev \
pkg-config \
nodejs \
tzdata \
git \

View File

@@ -35,53 +35,69 @@ namespace :tournament do
end
sleep(10)
@tournament.reload # Ensure matches association is fresh before iterating
@tournament.matches.reload.sort_by(&:bout_number).each do |match|
if match.reload.loser1_name != "BYE" and match.reload.loser2_name != "BYE" and match.reload.finished != 1
# Wait until both wrestlers are assigned
while (match.w1.nil? || match.w2.nil?)
puts "Waiting for wrestlers in match #{match.bout_number}..."
sleep(5) # Wait for 5 seconds before checking again
match.reload
end
puts "Finishing match with bout number #{match.bout_number}..."
loop do
@tournament.reload
@tournament.refill_open_bout_board_queues
# Choose a random winner
wrestlers = [match.w1, match.w2]
match.winner_id = wrestlers.sample
# Choose a random win type
win_type = WIN_TYPES.sample
match.win_type = win_type
# Assign score based on win type
match.score = case win_type
when "Decision"
low_score = rand(0..10)
high_score = low_score + rand(1..7)
"#{high_score}-#{low_score}"
when "Major"
low_score = rand(0..10)
high_score = low_score + rand(8..14)
"#{high_score}-#{low_score}"
when "Tech Fall"
low_score = rand(0..10)
high_score = low_score + rand(15..19)
"#{high_score}-#{low_score}"
when "Pin"
pin_times = ["0:30","1:12","5:37","2:34","3:54","4:23","5:56","0:12","1:00"]
pin_times.sample
else
"" # Default score
end
# Mark match as finished
match.finished = 1
match.save!
# sleep to prevent mysql locks when assign_next_match to a mat runs
sleep(0.5)
mats_with_queue1 = @tournament.mats.select do |mat|
match = mat.queue1_match
match && match.finished != 1 && match.loser1_name != "BYE" && match.loser2_name != "BYE"
end
break if mats_with_queue1.empty?
mat = mats_with_queue1.sample
match = mat.queue1_match
# Wait until both wrestlers are assigned for the selected queue1 match.
while match && (match.w1.nil? || match.w2.nil?)
puts "Waiting for wrestlers in match #{match.bout_number} on mat #{mat.name}..."
sleep(5)
@tournament.reload
@tournament.refill_open_bout_board_queues
match = mat.reload.queue1_match
end
next unless match
next if match.finished == 1 || match.loser1_name == "BYE" || match.loser2_name == "BYE"
puts "Finishing queue1 match on mat #{mat.name} with bout number #{match.bout_number}..."
# Choose a random winner
wrestlers = [match.w1, match.w2]
match.winner_id = wrestlers.sample
# Choose a random win type
win_type = WIN_TYPES.sample
match.win_type = win_type
# Assign score based on win type
match.score = case win_type
when "Decision"
low_score = rand(0..10)
high_score = low_score + rand(1..7)
"#{high_score}-#{low_score}"
when "Major"
low_score = rand(0..10)
high_score = low_score + rand(8..14)
"#{high_score}-#{low_score}"
when "Tech Fall"
low_score = rand(0..10)
high_score = low_score + rand(15..19)
"#{high_score}-#{low_score}"
when "Pin"
pin_times = ["0:30","1:12","5:37","2:34","3:54","4:23","5:56","0:12","1:00"]
pin_times.sample
else
""
end
# Mark match as finished
match.finished = 1
match.save!
# sleep to prevent mysql locks when queue advancement runs
sleep(0.5)
end
end
end

View File

@@ -35,12 +35,35 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
min-height: 100vh;
min-height: 100dvh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
#error-description {
fill: #d30001;
}
#error-id {
fill: #f0eff0;
}
@media (prefers-color-scheme: dark) {
body {
background: #101010;
color: #e0e0e0;
}
#error-description {
fill: #FF6161;
}
#error-id {
fill: #2c2c2c;
}
}
a {
color: inherit;
font-weight: 700;
@@ -83,13 +106,11 @@
}
main article br {
display: none;
@media(min-width: 48em) {
display: inline;
}
}
</style>
@@ -102,10 +123,10 @@
<main>
<header>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" fill="#f0eff0"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" fill="#d30001"/></svg>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" id="error-id"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" id="error-description"/></svg>
</header>
<article>
<p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If youre the application owner check the logs for more information.</p>
<p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If you're the application owner check the logs for more information.</p>
</article>
</main>

View File

@@ -4,7 +4,7 @@
<head>
<title>The page you were looking for doesnt exist (404 Not found)</title>
<title>The page you were looking for doesn't exist (404 Not found)</title>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, width=device-width">
@@ -35,12 +35,35 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
min-height: 100vh;
min-height: 100dvh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
#error-description {
fill: #d30001;
}
#error-id {
fill: #f0eff0;
}
@media (prefers-color-scheme: dark) {
body {
background: #101010;
color: #e0e0e0;
}
#error-description {
fill: #FF6161;
}
#error-id {
fill: #2c2c2c;
}
}
a {
color: inherit;
font-weight: 700;
@@ -83,13 +106,11 @@
}
main article br {
display: none;
@media(min-width: 48em) {
display: inline;
}
}
</style>
@@ -102,10 +123,10 @@
<main>
<header>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm165.328-35.41581-45.689 100.02991h26.224v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.184v-31.901l50.285-103.27391z" fill="#f0eff0"/><path d="m157.758 68.9967v34.0033h-7.199l-14.233-19.8814v19.8814h-8.584v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.528 0c0-3.4336-1.496-5.8703-4.209-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.209-2.3813 4.209-5.8149zm13.184 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm37.027 8.5839h-8.806v-34.0033h23.924v7.6978h-15.118v6.7564h13.9v7.5316h-13.9zm41.876-12.4605c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm35.337-12.4605v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.997 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm4.076 24.921v-24.921h8.694v2.1598c1.385-1.5506 3.822-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.639v-14.2327c0-2.049-1.053-3.5443-3.268-3.5443-1.717 0-3.156.9969-3.6 2.7136v15.0634zm44.113 0v-1.994c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.8151-11.132-13.0145s3.932-13.0143 11.132-13.0143c2.547 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.665-1.3291-2.16-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.717 0 3.156-.9415 3.821-2.326z" fill="#d30001"/></svg>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm165.328-35.41581-45.689 100.02991h26.224v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.184v-31.901l50.285-103.27391z" id="error-id"/><path d="m157.758 68.9967v34.0033h-7.199l-14.233-19.8814v19.8814h-8.584v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.528 0c0-3.4336-1.496-5.8703-4.209-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.209-2.3813 4.209-5.8149zm13.184 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm37.027 8.5839h-8.806v-34.0033h23.924v7.6978h-15.118v6.7564h13.9v7.5316h-13.9zm41.876-12.4605c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm35.337-12.4605v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.997 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm4.076 24.921v-24.921h8.694v2.1598c1.385-1.5506 3.822-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.639v-14.2327c0-2.049-1.053-3.5443-3.268-3.5443-1.717 0-3.156.9969-3.6 2.7136v15.0634zm44.113 0v-1.994c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.8151-11.132-13.0145s3.932-13.0143 11.132-13.0143c2.547 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.665-1.3291-2.16-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.717 0 3.156-.9415 3.821-2.326z" id="error-description"/></svg>
</header>
<article>
<p><strong>The page you were looking for doesnt exist.</strong> You may have mistyped the address or the page may have moved. If youre the application owner check the logs for more information.</p>
<p><strong>The page you were looking for doesn't exist.</strong> You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.</p>
</article>
</main>

View File

@@ -35,12 +35,35 @@
font-weight: 400;
letter-spacing: -0.0025em;
line-height: 1.4;
min-height: 100vh;
min-height: 100dvh;
place-items: center;
text-rendering: optimizeLegibility;
-webkit-text-size-adjust: 100%;
}
#error-description {
fill: #d30001;
}
#error-id {
fill: #f0eff0;
}
@media (prefers-color-scheme: dark) {
body {
background: #101010;
color: #e0e0e0;
}
#error-description {
fill: #FF6161;
}
#error-id {
fill: #2c2c2c;
}
}
a {
color: inherit;
font-weight: 700;
@@ -83,13 +106,11 @@
}
main article br {
display: none;
@media(min-width: 48em) {
display: inline;
}
}
</style>
@@ -102,7 +123,7 @@
<main>
<header>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm202.906 9.7326h-41.093c-2.433-7.2994-7.84-12.4361-17.302-12.4361-16.221 0-25.413 17.5728-25.954 34.8752v1.3517c5.137-7.0291 16.221-12.4361 30.82-12.4361 33.524 0 54.881 24.0612 54.881 53.7998 0 33.253-23.791 58.396-61.64 58.396-21.628 0-39.741-10.003-50.825-27.576-9.733-14.599-13.788-32.442-13.788-54.3406 0-51.9072 24.331-89.485807 66.236-89.485807 32.712 0 53.258 18.654107 58.665 47.851907zm-82.727 66.2355c0 13.247 9.463 22.439 22.71 22.439 12.977 0 22.439-9.192 22.439-22.439 0-13.517-9.462-22.7091-22.439-22.7091-13.247 0-22.71 9.1921-22.71 22.7091z" fill="#f0eff0"/><path d="m100.761 68.9967v34.0033h-7.1991l-14.2326-19.8814v19.8814h-8.5839v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm13.185 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.02-25.4194h9.083l12.958 34.0033h-9.027l-2.436-6.5902h-12.35l-2.381 6.5902h-8.806zm4.431 10.5222-3.489 9.5807h6.978zm17.44 11.0206c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm25.676 0c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm42.013 3.7658h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm23.4 16.7244v10.799h-8.694v-33.726h8.694v1.9937c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8703 11.131 13.0143 0 7.0886-3.932 13.0145-11.131 13.0145-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.16 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.717 0-3.157.9969-3.822 2.326zm21.892 7.1994v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.458 8.5839h-8.363v-1.274c-.83.831-3.322 1.717-5.981 1.717-4.928 0-9.082-2.769-9.082-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.045-2.9905-1.606 0-2.548.7199-2.936 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.553-1.0522-2.049-1.7167-3.655-1.7167-1.716 0-3.433.7199-3.433 2.3813 0 1.7168 1.717 2.4367 3.433 2.4367 1.606 0 3.102-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.694v-35.997h8.694v13.0697c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8149 11.131 13.0143s-3.932 13.0145-11.131 13.0145c-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.662 0-3.157.9969-3.822 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997z" fill="#d30001"/></svg>
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm202.906 9.7326h-41.093c-2.433-7.2994-7.84-12.4361-17.302-12.4361-16.221 0-25.413 17.5728-25.954 34.8752v1.3517c5.137-7.0291 16.221-12.4361 30.82-12.4361 33.524 0 54.881 24.0612 54.881 53.7998 0 33.253-23.791 58.396-61.64 58.396-21.628 0-39.741-10.003-50.825-27.576-9.733-14.599-13.788-32.442-13.788-54.3406 0-51.9072 24.331-89.485807 66.236-89.485807 32.712 0 53.258 18.654107 58.665 47.851907zm-82.727 66.2355c0 13.247 9.463 22.439 22.71 22.439 12.977 0 22.439-9.192 22.439-22.439 0-13.517-9.462-22.7091-22.439-22.7091-13.247 0-22.71 9.1921-22.71 22.7091z" id="error-id"/><path d="m100.761 68.9967v34.0033h-7.1991l-14.2326-19.8814v19.8814h-8.5839v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm13.185 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.02-25.4194h9.083l12.958 34.0033h-9.027l-2.436-6.5902h-12.35l-2.381 6.5902h-8.806zm4.431 10.5222-3.489 9.5807h6.978zm17.44 11.0206c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm25.676 0c0-7.6978 5.095-13.0143 12.572-13.0143 6.701 0 10.854 3.9874 11.574 9.8023h-8.418c-.221-1.4953-1.384-2.6029-3.156-2.6029-2.437 0-3.988 2.2706-3.988 5.8149s1.551 5.7595 3.988 5.7595c1.772 0 2.935-1.0522 3.156-2.5475h8.418c-.72 5.7596-4.873 9.8025-11.574 9.8025-7.477 0-12.572-5.3167-12.572-13.0145zm42.013 3.7658h8.031c-.887 5.7597-5.206 9.2487-11.686 9.2487-7.642 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.317-13.0143 12.516-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.319 4.5965 1.773 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm23.4 16.7244v10.799h-8.694v-33.726h8.694v1.9937c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8703 11.131 13.0143 0 7.0886-3.932 13.0145-11.131 13.0145-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.16 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.717 0-3.157.9969-3.822 2.326zm21.892 7.1994v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.206v6.7564h-5.206v8.307c0 1.9383.941 2.769 2.658 2.769.942 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm39.458 8.5839h-8.363v-1.274c-.83.831-3.322 1.717-5.981 1.717-4.928 0-9.082-2.769-9.082-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.482 1.3845v-1.606c0-1.606-.941-2.9905-3.045-2.9905-1.606 0-2.548.7199-2.936 1.8275h-8.196c.72-4.8181 4.984-8.6393 11.408-8.6393 7.089 0 11.132 3.7659 11.132 10.2453zm-8.363-6.9779v-1.4399c-.553-1.0522-2.049-1.7167-3.655-1.7167-1.716 0-3.433.7199-3.433 2.3813 0 1.7168 1.717 2.4367 3.433 2.4367 1.606 0 3.102-.6645 3.655-1.6614zm20.742 4.9839v1.994h-8.694v-35.997h8.694v13.0697c1.163-1.3291 3.6-2.5475 6.148-2.5475 7.199 0 11.131 5.8149 11.131 13.0143s-3.932 13.0145-11.131 13.0145c-2.548 0-4.985-1.219-6.148-2.548zm0-13.7893v6.5902c.665 1.3845 2.105 2.326 3.822 2.326 2.99 0 4.762-2.3814 4.762-5.5934s-1.772-5.6488-4.762-5.6488c-1.662 0-3.157.9969-3.822 2.326zm28.759-20.2137v35.997h-8.695v-35.997zm19.172 27.3023h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.516-13.0143 7.642 0 11.962 5.095 11.962 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.157-.7753 3.655-2.4921zm-3.821-10.0237c-2.049 0-3.434 1.2737-3.988 3.5997h7.532c-.111-2.0491-1.384-3.5997-3.544-3.5997z" id="error-description"/></svg>
</header>
<article>
<p><strong>Your browser is not supported.</strong><br> Please upgrade your browser to continue.</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More