mirror of
https://github.com/jcwimer/wrestlingApp
synced 2026-03-25 01:14:43 +00:00
Compare commits
244 Commits
9dae57e141
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 7526148ba5 | |||
| e8e0fa291b | |||
| 679fc2fcb9 | |||
| 18d39c6c8f | |||
| ca4d5ce0db | |||
| 654cb84827 | |||
| dc50efe8fc | |||
| 8670ce38c3 | |||
| d359be3ea1 | |||
| e97aa0d680 | |||
| ae8d995b2c | |||
| d57aaac09d | |||
| fcc8a9b9a9 | |||
| b51866e9d8 | |||
| 07d43e7720 | |||
| d8b6cfa8ac | |||
| 5d674f894f | |||
| 25df2a7280 | |||
| 2767274066 | |||
| a2f8c7bced | |||
| 9c2a9d62ad | |||
| 556090c16b | |||
| 86f9c03991 | |||
| c8764c149b | |||
| fe9a9c628c | |||
| 7e4b6d8fc8 | |||
| 940f7b1d00 | |||
| 52df73d14f | |||
| 8b03a74b1e | |||
| b4bca8f10a | |||
| af1f8df4b6 | |||
| 3576445a1c | |||
| 8c2ddf55ed | |||
| cfd3e7aecd | |||
| 608999cb51 | |||
| 6b5308360e | |||
| 9ca6572d9b | |||
| 61dc5e3cdd | |||
| af2fc3feba | |||
| 793a9e3ecc | |||
| f73e9bfc4e | |||
| 92bd06fe3c | |||
| 6e9554be55 | |||
| 34f1783031 | |||
| bbd2bd9b44 | |||
| 6ecebba70d | |||
| e64751e471 | |||
| d0f19e855f | |||
| 3e1ae22b6b | |||
| 15f85e439c | |||
| c5b9783853 | |||
| cd77268070 | |||
| d61ed80287 | |||
| dd5ce9bd60 | |||
| 9a4e6f6597 | |||
| 782baedcfe | |||
|
|
53e16952bf | ||
|
|
0326d87261 | ||
| 5296b71bb9 | |||
| 58be6b8074 | |||
| 4accedbb43 | |||
| 2856060b11 | |||
| 68a7b214c9 | |||
| 1fcaec876f | |||
| 3e4317dbc5 | |||
| 44fb5388b4 | |||
| ed7186e5ce | |||
| 6e61a7245a | |||
| 4828d9b876 | |||
| 2d433b680a | |||
| 9c25a6cc39 | |||
| f32e711d2b | |||
| 010d9a5f6b | |||
| 91e1939e69 | |||
| 934b34d0b7 | |||
| 63b91f7ddd | |||
| dd0585ed55 | |||
| 735f7090b2 | |||
| 690e497654 | |||
| 54655a2ea9 | |||
| 5114ed7b08 | |||
| f6bc7aa1a4 | |||
| 3248fdf7ca | |||
| 60814fdd94 | |||
| cc62e1c2f1 | |||
| d57b2e6e6f | |||
| fd1a7c43ff | |||
| b241818c21 | |||
| 288cb6704e | |||
| c45ec8ab38 | |||
| e612a4b10e | |||
| 38785924ed | |||
| 275847befe | |||
| b5150a3a37 | |||
| 6adcc709e7 | |||
| bb3b05ee81 | |||
| 490bd762f6 | |||
| 210e763d4c | |||
| f3e0f5d4c5 | |||
| 50055fc278 | |||
| 49fbf6735d | |||
| 698576dac9 | |||
| e986ce225b | |||
| 15bf39014f | |||
| 13bb8067fb | |||
| 4f0f69223d | |||
| 7e6d7ddfbb | |||
| 6242100e01 | |||
| d34fd873c0 | |||
| a851436c0c | |||
| 5f049793c8 | |||
| fc43de71bb | |||
| 5219c2b1e3 | |||
| f18802a933 | |||
| f6ef471591 | |||
| f92faf4f5c | |||
| db440c0603 | |||
| 5d37e5e0d8 | |||
| ad8e486205 | |||
| bb548be81b | |||
| b1f8cc3532 | |||
| b974773289 | |||
| 1f18f338ff | |||
| 6cb523e350 | |||
| b108545034 | |||
| e46180e9ea | |||
| fc3623008b | |||
| 508dd493a1 | |||
| 92bd1ec3c9 | |||
| 87353da05e | |||
| 57baa3ea45 | |||
| 2eb38ce788 | |||
| e047383fe4 | |||
| 05b42dbf0e | |||
| f011dae730 | |||
| 1078dc9e97 | |||
| c133a8b051 | |||
| 1d0cff0e6f | |||
| 1ee886abd3 | |||
| 86bb0b8410 | |||
| c3480909a2 | |||
| bde2b6d8c4 | |||
| 288c144f53 | |||
| 371b44977f | |||
| eb56b9d16c | |||
| 6b57246080 | |||
| ffb7d8be5b | |||
| 22f5733160 | |||
| b5ab929270 | |||
| 1d646cb05d | |||
| c328bbd91c | |||
| d675337d7a | |||
| 2dccf8dd55 | |||
| 6eb71ef59e | |||
| 67bee921ab | |||
| 8672bdd73d | |||
| 9c1563febf | |||
| 994fc18365 | |||
| 645fb59e5b | |||
| 05acebbf5b | |||
| 9c5ac6c1aa | |||
| 85b0da6a30 | |||
| 907a2eadef | |||
| e6797fcce9 | |||
| add48b95c3 | |||
| 85701e194a | |||
| 4d3f93a109 | |||
| f9095d8c99 | |||
| a3724914aa | |||
| 6e712cd199 | |||
| a3391afe02 | |||
| cce2e5b5f8 | |||
| 2cd62bbbd5 | |||
| d6a273c964 | |||
| 78bd51cafb | |||
| ae70128479 | |||
| 9fec6c079f | |||
| 58d088907a | |||
| 7220ffe3c6 | |||
| 0c9349c871 | |||
| d42b683f67 | |||
| f0e8c99b9f | |||
| 6db1dacbb6 | |||
| eae2c6756c | |||
| 194fbca978 | |||
| 1c8e3af5f4 | |||
| b571bcc749 | |||
| a6cc05424c | |||
| efeea574ed | |||
| 54e755ba3a | |||
| 8430949a37 | |||
| 5ef27f19cc | |||
| 09651ff12e | |||
| c0ad06cea7 | |||
| ad259f00b5 | |||
| 7ee8abe81a | |||
| cc38c842e0 | |||
| f7ea68da17 | |||
| cb5f0e28ae | |||
| 873e5b9465 | |||
| df6a029ef1 | |||
| 03d884e672 | |||
| e1cba865b0 | |||
| e36de59971 | |||
| 329c01db79 | |||
| 4cdc9e7df1 | |||
| 1f43fdf8fa | |||
| f6aff5a753 | |||
| 3dabc16a82 | |||
| 9d51ef7b51 | |||
| 63b0541aa4 | |||
| 1e30344be8 | |||
| 683b2967af | |||
| 432903e7a9 | |||
| 537eccf04d | |||
| 89a695388a | |||
| 0aea26967a | |||
| 5677f4e944 | |||
| f5ddc6652d | |||
| 987c89b7d5 | |||
| b2ba1901df | |||
| 6c86f25add | |||
| ce063f5faa | |||
| 396e4be5b3 | |||
| 46919a2b1b | |||
| 55d122771c | |||
| e43f3253d4 | |||
| f720de0db6 | |||
| 44c9f947d7 | |||
| ee8d861bee | |||
| bdd80fc1d2 | |||
| db15e79c0f | |||
| 1f5aa304ff | |||
| 4522113396 | |||
| 7327902fe8 | |||
| 894877e657 | |||
| aa76318321 | |||
| a693905c62 | |||
| 3559977e72 | |||
| 4a43f5e53f | |||
| 6d5f6d39f7 | |||
| d2c68d7c6b | |||
| afaae0fd12 | |||
| eac603667b |
11
.cursorrules
Normal file
11
.cursorrules
Normal file
@@ -0,0 +1,11 @@
|
||||
- If rails isn't installed use docker: docker run -it -v $(pwd):/rails wrestlingdev-dev <rails command>
|
||||
- If the docker image doesn't exist, use the build command: docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile .
|
||||
- If the Gemfile changes, you need to rebuild the docker image: docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile.
|
||||
- Do not add unnecessary comments to the code where you remove things.
|
||||
- Cypress tests are created for js tests. They can be found in cypress-tests/cypress
|
||||
- Cypress tests can be run with docker: bash cypress-tests/run-cypress-tests.sh
|
||||
- Rails tests can be run with docker: docker run -it -v $(pwd):/rails wrestlingdev-dev rake test
|
||||
- Write as little code as possible. I do not want crazy non standard rails implementations.
|
||||
- This project is using propshaft and importmap.
|
||||
- Stimulus is used for javascript.
|
||||
- use context7
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -11,10 +11,26 @@ vendor/bundle/
|
||||
# Ignore the default SQLite database.
|
||||
db/*.sqlite3
|
||||
db/*.sqlite3-journal
|
||||
db/*.sqlite3-shm
|
||||
db/*.sqlite3-wal
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
log/*.log
|
||||
log/*.log*
|
||||
tmp
|
||||
.rvmrc
|
||||
deploy/prod.env
|
||||
frontend/node_modules
|
||||
.aider*
|
||||
|
||||
# Ignore cypress test results
|
||||
cypress-tests/cypress/screenshots
|
||||
cypress-tests/cypress/videos
|
||||
|
||||
.DS_Store
|
||||
|
||||
# generated with npx repomix
|
||||
# repomix-output.xml
|
||||
|
||||
# generated by cine mcp settings
|
||||
~/
|
||||
1
.ruby-gemset
Normal file
1
.ruby-gemset
Normal file
@@ -0,0 +1 @@
|
||||
wrestlingdev
|
||||
1
.ruby-version
Normal file
1
.ruby-version
Normal file
@@ -0,0 +1 @@
|
||||
ruby-4.0.1
|
||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
||||
sudo: required
|
||||
language: minimal
|
||||
services:
|
||||
- docker
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- docker-ce
|
||||
env:
|
||||
- DOCKER_COMPOSE_VERSION=1.22.0
|
||||
script:
|
||||
- bash bin/run-tests-with-docker.sh
|
||||
deploy:
|
||||
provider: heroku
|
||||
api_key:
|
||||
secure: WQnMF1v9J8n3z+Icx1Sp2tcu5bsIDwuCRgmGSEyFEl0aI3KsFxCpNWKEhHougkBxYRi7XXW1TZGGwRYb5VNf5UVG4xqlgJE7vm4ri3PjU2x/bLJb6tJq+WNrXoJNzfeyRqwXpfOYJT6n3/ak7lsZrVY2zSIuNTdQQ1oVWk33x9KNyr1RS/XmygJDnsG8n7dnz4xUi57F2w3hORVF3Lm3a63F9hoBcZjZUeMHzLPPhG4yySkpBe1oWtFk58ZSyqCSpcpbiQSSCxHiMrlSJ6GDZjUFDe+GIkx/P8by+MP0qcS2dw1w5yPZvsAATe826xP+LmcZX7g2LHJbIDu+ZwisQDbWfhpShvIkgtnhG95fAF7pv+md6VsLf3cTggtOYKHXDGBTO2nHDDuol/W7ZZHiVT5Da52MFdkJ/4TTgzqWmnlDmmJT6nAZKgGp/dcnslUHscwM1nnhJZZqbxbg8tIZ3Q9+hLjh9vikO76ujkIaseJ+fGcpzTl5SvwW7NfINzJPkVZsPQb6tYNs01iKVfLJ8xNKvUswKe3G3nvrbfJahgySZ0+4xDEjQbbaa63RjyOw06DAcN3SgMj0o1w66NGdd1GzloggN0mdUfXkn+mjP3okYh7zgPY1n+ZJ88BQYJoScS790g30pqxR1Tj0uR3+TEd3Qmp7McfXKlMJiMXX2mI=
|
||||
app: wrestlingapp
|
||||
on:
|
||||
repo: jcwimer/wrestlingApp
|
||||
branch: master
|
||||
run: rake db:migrate RAILS_ENV=production
|
||||
71
Gemfile
71
Gemfile
@@ -1,23 +1,36 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '2.6.5'
|
||||
ruby '4.0.1'
|
||||
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
|
||||
gem 'rails', '6.0.1'
|
||||
# Use sqlite3 as the database for Active Record
|
||||
#gem 'sqlite3', '~> 1.3', '< 1.4', :group => :development
|
||||
gem 'sqlite3', :group => :development
|
||||
gem 'rails', '8.1.2'
|
||||
|
||||
# Added in rails 7.1
|
||||
gem 'rails-html-sanitizer'
|
||||
|
||||
# Asset Management: Propshaft for serving, Importmap for JavaScript
|
||||
gem "propshaft"
|
||||
gem "importmap-rails"
|
||||
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem "bootsnap", require: false
|
||||
|
||||
# Use sqlite3 as the database for Active Record
|
||||
# Use sqlite3 version compatible with Rails 8
|
||||
gem 'sqlite3', ">= 2.1", :group => :development
|
||||
|
||||
# JavaScript and CSS related gems
|
||||
# Uglifier is not used with Propshaft by default
|
||||
# CoffeeScript (.js.coffee) files need to be converted to .js as Propshaft doesn't compile them
|
||||
|
||||
# Use Uglifier as compressor for JavaScript assets
|
||||
gem 'uglifier'
|
||||
# Use CoffeeScript for .js.coffee assets and views
|
||||
gem 'coffee-rails'
|
||||
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
|
||||
# gem 'therubyracer', platforms: :ruby
|
||||
|
||||
# Use jquery as the JavaScript library
|
||||
gem 'jquery-rails'
|
||||
# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
|
||||
gem 'turbolinks'
|
||||
# Turbo for modern page interactions
|
||||
gem 'turbo-rails'
|
||||
# Stimulus for JavaScript behaviors
|
||||
gem 'stimulus-rails'
|
||||
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
||||
gem 'jbuilder'
|
||||
# bundle exec rake doc:rails generates the API under doc/api.
|
||||
@@ -27,7 +40,7 @@ gem 'sdoc', :group => :doc
|
||||
gem 'spring', :group => :development
|
||||
|
||||
# Use ActiveModel has_secure_password
|
||||
# gem 'bcrypt', '~> 3.1.7'
|
||||
gem 'bcrypt', '~> 3.1.7'
|
||||
|
||||
# Use unicorn as the app server
|
||||
# gem 'unicorn'
|
||||
@@ -38,32 +51,44 @@ gem 'spring', :group => :development
|
||||
# Use debugger
|
||||
# gem 'debugger', group: [:development, :test]
|
||||
|
||||
|
||||
#Installed by me
|
||||
group :production do
|
||||
gem 'rails_12factor'
|
||||
gem 'mysql2'
|
||||
gem 'therubyracer'
|
||||
gem 'newrelic_rpm'
|
||||
gem 'dalli'
|
||||
end
|
||||
|
||||
gem 'devise'
|
||||
gem 'solid_cache'
|
||||
|
||||
gem 'influxdb-rails'
|
||||
# Authentication
|
||||
# gem 'devise' # Removed - replaced with Rails built-in authentication
|
||||
|
||||
# Role Management
|
||||
gem 'cancancan'
|
||||
gem 'round_robin_tournament'
|
||||
gem 'rb-readline'
|
||||
gem 'delayed_job_active_record'
|
||||
gem 'rqrcode'
|
||||
# Replacing Delayed Job with Solid Queue
|
||||
# gem 'delayed_job_active_record'
|
||||
gem 'solid_queue'
|
||||
gem 'solid_cable'
|
||||
gem 'puma'
|
||||
gem 'passenger'
|
||||
gem 'tzinfo-data'
|
||||
gem 'daemons'
|
||||
gem 'delayed_job_web'
|
||||
# Solid Queue UI
|
||||
gem "mission_control-jobs"
|
||||
|
||||
|
||||
group :development do
|
||||
gem 'rubocop'
|
||||
# gem 'rubocop'
|
||||
gem 'bullet'
|
||||
gem 'brakeman'
|
||||
gem 'hakiri'
|
||||
gem 'travis'
|
||||
gem 'bundler-audit'
|
||||
gem 'rubocop'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
gem 'mocha'
|
||||
# rails-controller-testing is needed for assert_template
|
||||
gem 'rails-controller-testing'
|
||||
end
|
||||
|
||||
615
Gemfile.lock
615
Gemfile.lock
@@ -1,335 +1,390 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (6.0.1)
|
||||
actionpack (= 6.0.1)
|
||||
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)
|
||||
actionmailbox (6.0.1)
|
||||
actionpack (= 6.0.1)
|
||||
activejob (= 6.0.1)
|
||||
activerecord (= 6.0.1)
|
||||
activestorage (= 6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.1)
|
||||
actionpack (= 6.0.1)
|
||||
actionview (= 6.0.1)
|
||||
activejob (= 6.0.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.1)
|
||||
actionview (= 6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
rack (~> 2.0)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.1)
|
||||
actionpack (= 6.0.1)
|
||||
activerecord (= 6.0.1)
|
||||
activestorage (= 6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
zeitwerk (~> 2.6)
|
||||
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.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.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
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.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
activerecord (6.0.1)
|
||||
activemodel (= 6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
activestorage (6.0.1)
|
||||
actionpack (= 6.0.1)
|
||||
activejob (= 6.0.1)
|
||||
activerecord (= 6.0.1)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (6.0.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
zeitwerk (~> 2.2)
|
||||
addressable (2.4.0)
|
||||
ast (2.4.0)
|
||||
backports (3.15.0)
|
||||
bcrypt (3.1.13)
|
||||
brakeman (4.7.1)
|
||||
builder (3.2.3)
|
||||
bullet (6.0.2)
|
||||
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.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.1.2)
|
||||
base64
|
||||
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)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
ast (2.4.3)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.21)
|
||||
bigdecimal (4.0.1)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (8.0.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
cancancan (3.0.1)
|
||||
coffee-rails (5.0.0)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (>= 5.2.0)
|
||||
coffee-script (2.4.1)
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.12.2)
|
||||
commander (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
concurrent-ruby (1.1.5)
|
||||
crass (1.0.5)
|
||||
daemons (1.3.1)
|
||||
dalli (2.7.10)
|
||||
delayed_job (4.1.8)
|
||||
activesupport (>= 3.0, < 6.1)
|
||||
delayed_job_active_record (4.1.4)
|
||||
activerecord (>= 3.0, < 6.1)
|
||||
delayed_job (>= 3.0, < 5)
|
||||
delayed_job_web (1.4.3)
|
||||
activerecord (> 3.0.0)
|
||||
delayed_job (> 2.0.3)
|
||||
rack-protection (>= 1.5.5)
|
||||
sinatra (>= 1.4.4)
|
||||
devise (4.7.1)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
erubi (1.9.0)
|
||||
ethon (0.12.0)
|
||||
ffi (>= 1.3.0)
|
||||
execjs (2.7.0)
|
||||
faraday (0.17.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday_middleware (0.13.1)
|
||||
faraday (>= 0.7.4, < 1.0)
|
||||
ffi (1.11.2)
|
||||
gh (0.15.1)
|
||||
addressable (~> 2.4.0)
|
||||
backports
|
||||
faraday (~> 0.8)
|
||||
multi_json (~> 1.0)
|
||||
net-http-persistent (~> 2.9)
|
||||
net-http-pipeline
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
hakiri (0.7.2)
|
||||
activesupport
|
||||
bundler
|
||||
commander
|
||||
i18n
|
||||
json
|
||||
rake
|
||||
rest-client
|
||||
terminal-table
|
||||
highline (1.7.10)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
i18n (1.7.0)
|
||||
bundler-audit (0.9.3)
|
||||
bundler (>= 1.2.0)
|
||||
thor (~> 1.0)
|
||||
cancancan (3.6.1)
|
||||
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.5.1)
|
||||
drb (2.2.3)
|
||||
erb (6.0.1)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
fugit (1.12.1)
|
||||
et-orbi (~> 1.4)
|
||||
raabro (~> 1.4)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jaro_winkler (1.5.4)
|
||||
jbuilder (2.9.1)
|
||||
activesupport (>= 4.2.0)
|
||||
jquery-rails (4.3.5)
|
||||
importmap-rails (2.2.3)
|
||||
actionpack (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
influxdb (0.8.1)
|
||||
influxdb-rails (1.0.3)
|
||||
influxdb (~> 0.6, >= 0.6.4)
|
||||
railties (>= 5.0)
|
||||
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.1)
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.2.0)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
libv8 (3.16.14.19)
|
||||
loofah (2.3.1)
|
||||
json (2.18.1)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
loofah (2.25.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.9.0)
|
||||
logger
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (0.3.3)
|
||||
mimemagic (~> 0.3.2)
|
||||
method_source (0.9.2)
|
||||
mime-types (3.3)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2019.1009)
|
||||
mimemagic (0.3.3)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.13.0)
|
||||
multi_json (1.14.1)
|
||||
multipart-post (2.1.1)
|
||||
mustermann (1.0.3)
|
||||
mysql2 (0.5.2)
|
||||
net-http-persistent (2.9.4)
|
||||
net-http-pipeline (1.0.1)
|
||||
netrc (0.11.0)
|
||||
newrelic_rpm (6.7.0.359)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.5)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.19.0)
|
||||
parser (2.6.5.0)
|
||||
ast (~> 2.4.0)
|
||||
passenger (6.0.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.1.0)
|
||||
mini_mime (1.1.5)
|
||||
minitest (6.0.1)
|
||||
prism (~> 1.5)
|
||||
mission_control-jobs (1.1.0)
|
||||
actioncable (>= 7.1)
|
||||
actionpack (>= 7.1)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
importmap-rails (>= 1.2.1)
|
||||
irb (~> 1.13)
|
||||
railties (>= 7.1)
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mocha (3.0.2)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
mysql2 (0.5.7)
|
||||
bigdecimal
|
||||
net-imap (0.6.3)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.0-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.9.0)
|
||||
propshaft (1.3.1)
|
||||
actionpack (>= 7.0.0)
|
||||
activesupport (>= 7.0.0)
|
||||
rack
|
||||
rake (>= 0.8.1)
|
||||
puma (4.3.0)
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
pusher-client (0.6.2)
|
||||
json
|
||||
websocket (~> 1.0)
|
||||
rack (2.0.7)
|
||||
rack-protection (2.0.7)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.0.1)
|
||||
actioncable (= 6.0.1)
|
||||
actionmailbox (= 6.0.1)
|
||||
actionmailer (= 6.0.1)
|
||||
actionpack (= 6.0.1)
|
||||
actiontext (= 6.0.1)
|
||||
actionview (= 6.0.1)
|
||||
activejob (= 6.0.1)
|
||||
activemodel (= 6.0.1)
|
||||
activerecord (= 6.0.1)
|
||||
activestorage (= 6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.1)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
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.3.1)
|
||||
rack (>= 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.1.2)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails_12factor (0.0.3)
|
||||
rails_serve_static_assets
|
||||
rails_stdout_logging
|
||||
rails_serve_static_assets (0.0.5)
|
||||
rails_stdout_logging (0.0.5)
|
||||
railties (6.0.1)
|
||||
actionpack (= 6.0.1)
|
||||
activesupport (= 6.0.1)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.1)
|
||||
railties (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
rb-readline (0.5.5)
|
||||
rdoc (6.2.0)
|
||||
ref (2.0.0)
|
||||
responders (3.0.0)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rest-client (2.1.0)
|
||||
http-accept (>= 1.7.0, < 2.0)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
round_robin_tournament (0.0.1)
|
||||
rubocop (0.76.0)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
rdoc (7.2.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
round_robin_tournament (0.1.2)
|
||||
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)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.6)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.49.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
ruby-progressbar (1.10.1)
|
||||
sdoc (1.0.0)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.49.0)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
sdoc (2.6.5)
|
||||
rdoc (>= 5.0)
|
||||
sinatra (2.0.7)
|
||||
mustermann (~> 1.0)
|
||||
rack (~> 2.0)
|
||||
rack-protection (= 2.0.7)
|
||||
tilt (~> 2.0)
|
||||
spring (2.1.0)
|
||||
sprockets (4.0.0)
|
||||
securerandom (0.4.1)
|
||||
solid_cable (3.0.12)
|
||||
actioncable (>= 7.2)
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
solid_cache (1.0.10)
|
||||
activejob (>= 7.2)
|
||||
activerecord (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
solid_queue (1.3.1)
|
||||
activejob (>= 7.1)
|
||||
activerecord (>= 7.1)
|
||||
concurrent-ruby (>= 1.3.1)
|
||||
fugit (~> 1.11)
|
||||
railties (>= 7.1)
|
||||
thor (>= 1.3.1)
|
||||
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.2.0)
|
||||
thor (1.5.0)
|
||||
timeout (0.6.0)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.23)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.1)
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sqlite3 (1.4.1)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
therubyracer (0.12.3)
|
||||
libv8 (~> 3.16.14.15)
|
||||
ref
|
||||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
travis (1.8.10)
|
||||
backports
|
||||
faraday (~> 0.9)
|
||||
faraday_middleware (~> 0.9, >= 0.9.1)
|
||||
gh (~> 0.13)
|
||||
highline (~> 1.6)
|
||||
launchy (~> 2.1)
|
||||
pusher-client (~> 0.4)
|
||||
typhoeus (~> 0.6, >= 0.6.8)
|
||||
turbolinks (5.2.1)
|
||||
turbolinks-source (~> 5.2)
|
||||
turbolinks-source (5.2.0)
|
||||
typhoeus (0.8.0)
|
||||
ethon (>= 0.8.0)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2019.3)
|
||||
tzinfo-data (1.2025.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.0)
|
||||
uniform_notifier (1.13.0)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
websocket (1.2.8)
|
||||
websocket-driver (0.7.1)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uniform_notifier (1.18.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.4)
|
||||
zeitwerk (2.2.1)
|
||||
websocket-extensions (0.1.5)
|
||||
zeitwerk (2.7.4)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
aarch64-linux-gnu
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin
|
||||
x86_64-darwin
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
bcrypt (~> 3.1.7)
|
||||
bootsnap
|
||||
brakeman
|
||||
bullet
|
||||
bundler-audit
|
||||
cancancan
|
||||
coffee-rails
|
||||
daemons
|
||||
dalli
|
||||
delayed_job_active_record
|
||||
delayed_job_web
|
||||
devise
|
||||
hakiri
|
||||
importmap-rails
|
||||
influxdb-rails
|
||||
jbuilder
|
||||
jquery-rails
|
||||
mission_control-jobs
|
||||
mocha
|
||||
mysql2
|
||||
newrelic_rpm
|
||||
passenger
|
||||
propshaft
|
||||
puma
|
||||
rails (= 6.0.1)
|
||||
rails (= 8.1.2)
|
||||
rails-controller-testing
|
||||
rails-html-sanitizer
|
||||
rails_12factor
|
||||
rb-readline
|
||||
round_robin_tournament
|
||||
rqrcode
|
||||
rubocop
|
||||
sdoc
|
||||
solid_cable
|
||||
solid_cache
|
||||
solid_queue
|
||||
spring
|
||||
sqlite3
|
||||
therubyracer
|
||||
travis
|
||||
turbolinks
|
||||
sqlite3 (>= 2.1)
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
tzinfo-data
|
||||
uglifier
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.6.5p114
|
||||
ruby 4.0.1p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.0.2
|
||||
4.0.3
|
||||
|
||||
4
Procfile
4
Procfile
@@ -1,4 +0,0 @@
|
||||
worker: bundle exec bin/delayed_job -n 1 run
|
||||
#worker: bundle exec rake jobs:work
|
||||
#web: bundle exec puma -w 3 -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-development}
|
||||
web: bundle exec passenger start -p $PORT --max-pool-size 2 --min-instances 2
|
||||
228
README.md
228
README.md
@@ -1,56 +1,220 @@
|
||||
# README
|
||||
This application is being created to run a wrestling tournament.
|
||||
|
||||
### Current master status
|
||||
[](https://travis-ci.org/jcwimer/wrestlingApp)
|
||||
|
||||
### Current development status
|
||||
[](https://travis-ci.org/jcwimer/wrestlingApp)
|
||||
|
||||
# Info
|
||||
**License:** MIT License
|
||||
|
||||
**Public Production Url:** [https://wrestlingdev.com](http://wrestlingdev.com)
|
||||
**Public Production Url:** [https://wrestlingdev.com](https://wrestlingdev.com)
|
||||
|
||||
**App Info**
|
||||
* Ruby 2.6.5
|
||||
* Rails 6.0.1
|
||||
* DB mysql or mariadb
|
||||
* Memcached
|
||||
* Delayed Jobs
|
||||
* 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
|
||||
* Solid Cable -> MySQL/MariaDB for websocket channels
|
||||
* Hotwired Stimulus for client-side JavaScript
|
||||
|
||||
# Development
|
||||
|
||||
## Develop with docker
|
||||
All dependencies are wrapped in docker. Tests can be run with `bash bin/run-tests-with-docker.sh`. That is the same command used in CI.
|
||||
|
||||
If you want to run a full rails environment shell in docker run: `bash bin/rails-dev-run.sh wrestlingapp-dev`
|
||||
|
||||
If you want to run a full rails environment shell in docker run: `bash bin/rails-dev-run.sh wrestlingdev-dev`
|
||||
From here, you can run the normal rails commands.
|
||||
|
||||
Special rake tasks:
|
||||
* `tournament:assign_random_wins` will complete all matches for tournament 204 from seed data. This task takes a while since it waits for the worker to complete tasks. In my testing, it takes about 3.5 hours to complete.
|
||||
* `rake tournament:assign_random_wins` to run locally
|
||||
* `docker-compose -f deploy/docker-compose-test.yml exec -T app rails tournament:assign_random_wins` to run on the dev server
|
||||
|
||||
To deploy a full local version of the app `bash deploy/deploy-test.sh` (this requires docker-compose to be installed). This deploys a full version of the app including Rails app, Solid Queue background workers, Memcached, and MariaDB. Now, you can open [http://localhost](http://localhost).
|
||||
|
||||
In development environments, background jobs run inline (synchronously) by default. In production and staging environments, jobs are processed asynchronously by separate worker processes.
|
||||
|
||||
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` 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/'"` 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`
|
||||
Then, `cd ../; cd wrestlingApp`. This will load the gemset file in this repo.
|
||||
|
||||
## Quick Rails Commands Without Local Installation
|
||||
You can run one-off Rails commands without installing Rails locally by using the development Docker image:
|
||||
|
||||
```bash
|
||||
# Build the development image
|
||||
docker build -t wrestlingdev-dev -f deploy/rails-dev-Dockerfile .
|
||||
|
||||
# Run a specific Rails command
|
||||
docker run -it -v $(pwd):/rails wrestlingdev-dev rails db:migrate
|
||||
|
||||
# Run the Rails console
|
||||
docker run -it -v $(pwd):/rails wrestlingdev-dev rails console
|
||||
|
||||
# Run a custom Rake task
|
||||
docker run -it -v $(pwd):/rails wrestlingdev-dev rake jobs:create_running
|
||||
```
|
||||
|
||||
This approach is useful for quick commands without the need to set up a full development environment. The image contains all required dependencies for the application.
|
||||
|
||||
For a more convenient experience with a persistent shell, use the included wrapper script:
|
||||
|
||||
```bash
|
||||
bash bin/rails-dev-run.sh wrestlingdev-dev
|
||||
```
|
||||
|
||||
## Rails commands
|
||||
Whether you have a shell from docker or are using rvm you can now run normal rails commands:
|
||||
* `bundle config set --local without 'production'`
|
||||
* `bundle install`
|
||||
* `rake db:seed` Development login email from seed data: `test@test.com` password: `password`
|
||||
* `rake test`
|
||||
* `rails generate blah blah blah`
|
||||
* ` rails s -b 0.0.0.0` port 3000 is exposed. You can open [http://localhost:3000](http://localhost:3000) after running that command
|
||||
* etc.
|
||||
* `rake finish_seed_tournament` will complete all matches from the seed data. This command takes about 5 minutes to execute
|
||||
* `rake finish_seed_tournaments` will complete all matches from the seed data. This command takes about 5 minutes to execute
|
||||
* `rake assets:clobber` - removes previously compiled assets stored in `public/assets` forcing Rails to recompile them from scratch the next time they are requested.
|
||||
|
||||
To deploy a a full local version of the app `bash deploy/deploy-test.sh` (this requires docker-compose to be installed). This deploys a full version of the app. App, delayed job, memcached, and mariadb. Now, you can open [http://localhost](http://localhost). Delayed jobs are turned off in dev and everything that is a delayed job in prod just runs in browser.
|
||||
## Testing Job Status
|
||||
|
||||
To help with testing the job status display in the UI, several rake tasks are provided:
|
||||
|
||||
```bash
|
||||
# Create a test "Running" job for the first tournament
|
||||
rails jobs:create_running
|
||||
|
||||
# Create a test "Completed" job for the first tournament
|
||||
rails jobs:create_completed
|
||||
|
||||
# Create a test "Error" job for the first tournament
|
||||
rails jobs:create_failed
|
||||
```
|
||||
|
||||
See `SOLID_QUEUE.md` for more details about the job system.
|
||||
|
||||
## Update gems
|
||||
|
||||
1. `bash bin/rails-dev-run.sh wrestlingdev-dev` to open a contianer with a rails shell available
|
||||
2. `bundle config --delete without` to remove the bundle config that ignores production gems
|
||||
3. `bundle update`
|
||||
4. `bundle config set --local without 'production'` to reset your without locally
|
||||
|
||||
Note: If updating rails, do not change the version in `Gemfile` until after you run `bash bin/rails-dev-run.sh wrestlingdev-dev`. Creating the container will fail due to a mismatch in Gemfile and Gemfile.lock.
|
||||
Then run `rails app:update` to update rails.
|
||||
|
||||
## Stimulus Controllers
|
||||
|
||||
The application uses Hotwired Stimulus for client-side JavaScript interactivity. Controllers can be found in `app/asssets/javascripts/controllers`
|
||||
|
||||
### Testing Stimulus Controllers
|
||||
|
||||
The Stimulus controllers are tested using Cypress end-to-end tests:
|
||||
|
||||
```bash
|
||||
# Run Cypress tests in headless mode
|
||||
bash cypress-tests/run-cypress-tests.sh
|
||||
```
|
||||
|
||||
# Deployment
|
||||
|
||||
The production version of this is currently deployed in Kubernetes. See [Deploying with Kubernetes](deploy/kubernetes/README.md)
|
||||
The production version of this is currently deployed in Kubernetes (via K3s). See [Deploying with Kubernetes](deploy/kubernetes/README.md)
|
||||
|
||||
**Required environment variables for deployment**
|
||||
* `WRESTLINGDEV_DB_NAME=databasename`
|
||||
* `WRESTLINGDEV_DB_USER=databaseusername`
|
||||
* `WRESTLINGDEV_DB_PWD=databasepassword`
|
||||
* `WRESTLINGDEV_DB_HOST=database.homename`
|
||||
* `WRESTLINGDEV_DB_PORT=databaseport`
|
||||
* `WRESTLINGDEV_DEVISE_SECRET_KEY=devise_key` can be generated with `rake secret`
|
||||
* `WRESTLINGDEV_SECRET_KEY_BASE=secret_key` can be generated with `rake secret`
|
||||
* `WRESTLINGDEV_EMAIL_PWD=emailpwd` Email has to be a gmail account for now.
|
||||
* `WRESTLINGDEV_EMAIL=email address`
|
||||
I'm using a Hetzner dedicated server with an i7-8700, 500GB NVME (RAID1), and 64GB ECC RAM. I have a hot standby (SQL read only replication) in my homelab.
|
||||
|
||||
**Optional environment variables**
|
||||
* `MEMCACHIER_PASSWORD=memcachier_password` needed for caching password
|
||||
* `MEMCACHIER_SERVERS=memcachier_hostname:memcachier_port` needed for caching
|
||||
* `MEMCACHIER_USERNAME=memcachier_username` needed for caching
|
||||
* `WRESTLINGDEV_NEW_RELIC_LICENSE_KEY=new_relic_license_key` this is only needed to use new relic
|
||||
## Server Configuration
|
||||
|
||||
### Puma and SolidQueue
|
||||
|
||||
The application uses an intelligent auto-scaling configuration for Puma (the web server) and SolidQueue (background job processing):
|
||||
|
||||
- **Auto Detection**: The server automatically detects available CPU cores and memory, and scales accordingly.
|
||||
- **Worker Scaling**: In production, the number of Puma workers is calculated based on available memory (assuming ~400MB per worker) and CPU cores.
|
||||
- **Thread Configuration**: Each Puma worker uses 5-12 threads by default, optimized for mixed I/O and CPU workloads.
|
||||
- **SolidQueue Integration**: When `SOLID_QUEUE_IN_PUMA=true`, background jobs run within the Puma process.
|
||||
- **Database Connection Pool**: Automatically sized based on the maximum number of threads across all workers.
|
||||
|
||||
All of these settings can be overridden with environment variables if needed.
|
||||
|
||||
To see the current configuration in the logs, look for these lines on startup:
|
||||
```
|
||||
Puma starting with X worker(s), Y-Z threads per worker
|
||||
Available system resources: X CPU(s), YMMMB RAM
|
||||
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 WRESTLINGDEV_MISSION_CONTROL_USER/WRESTLINGDEV_MISSION_CONTROL_PASSWORD. You can see this in `config/environments/production.rb` and `config/environments/development.rb`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required Environment Variables
|
||||
* `WRESTLINGDEV_DB_NAME` - Database name for the main application
|
||||
* `WRESTLINGDEV_DB_USR` - Database username
|
||||
* `WRESTLINGDEV_DB_PWD` - Database password
|
||||
* `WRESTLINGDEV_DB_HOST` - Database hostname
|
||||
* `WRESTLINGDEV_DB_PORT` - Database port
|
||||
* `WRESTLINGDEV_DEVISE_SECRET_KEY` - Secret key for Devise (can be generated with `rake secret`)
|
||||
* `WRESTLINGDEV_SECRET_KEY_BASE` - Rails application secret key (can be generated with `rake secret`)
|
||||
* `WRESTLINGDEV_EMAIL` - Email address (currently must be a Gmail account)
|
||||
* `WRESTLINGDEV_EMAIL_PWD` - Email password
|
||||
* `WRESTLINGDEV_MISSION_CONTROL_USER` - mission control username
|
||||
* `WRESTLINGDEV_MISSION_CONTROL_PASSWORD` - mission control password
|
||||
|
||||
### Optional Environment Variables
|
||||
* `SOLID_QUEUE_IN_PUMA` - Set to "true" to run Solid Queue workers inside Puma (default in development)
|
||||
* `WEB_CONCURRENCY` - Number of Puma workers (auto-detected based on CPU/memory if not specified)
|
||||
* `RAILS_MIN_THREADS` - Minimum number of threads per Puma worker (defaults to 5)
|
||||
* `RAILS_MAX_THREADS` - Maximum number of threads per Puma worker (defaults to 12)
|
||||
* `DATABASE_POOL_SIZE` - Database connection pool size (auto-calculated if not specified)
|
||||
* `SOLID_QUEUE_WORKERS` - Number of SolidQueue workers (auto-calculated if not specified)
|
||||
* `SOLID_QUEUE_THREADS` - Number of threads per SolidQueue worker (auto-calculated if not specified)
|
||||
* `PORT` - Port for Puma server to listen on (defaults to 3000)
|
||||
* `RAILS_LOG_LEVEL` - Log level for Rails in production (defaults to "info")
|
||||
* `PIDFILE` - PID file location for Puma
|
||||
* `RAILS_SSL_TERMINATION` - Set to "true" to enable force_ssl in production (HTTPS enforcement)
|
||||
* `REVERSE_PROXY_SSL_TERMINATION` - Set to "true" if the app is behind a SSL-terminating reverse proxy
|
||||
* `CI` - Set in CI environments to enable eager loading in test environment
|
||||
* `WRESTLINGDEV_NEW_RELIC_LICENSE_KEY` - New Relic license key for monitoring
|
||||
|
||||
### InfluxDB Configuration (all required if using InfluxDB)
|
||||
* `WRESTLINGDEV_INFLUXDB_DATABASE` - InfluxDB database name
|
||||
* `WRESTLINGDEV_INFLUXDB_HOST` - InfluxDB hostname
|
||||
* `WRESTLINGDEV_INFLUXDB_PORT` - InfluxDB port
|
||||
* `WRESTLINGDEV_INFLUXDB_USERNAME` - InfluxDB username (optional)
|
||||
* `WRESTLINGDEV_INFLUXDB_PASSWORD` - InfluxDB password (optional)
|
||||
|
||||
This project provides multiple ways to develop and deploy, with Docker being the primary method.
|
||||
|
||||
# Frontend Assets
|
||||
|
||||
## Sprockets to Propshaft Migration
|
||||
|
||||
- Propshaft will automatically include in its search paths the folders vendor/assets, lib/assets and app/assets of your project and of all the gems in your Gemfile. You can see all included files by using the reveal rake task: `rake assets:reveal`. When importing you'll use the relative path from this command.
|
||||
- All css files are imported via `app/assets/stylesheets/application.css`. This is imported on `app/views/layouts/application.html.erb`.
|
||||
- Bootstrap and fontawesome have been downloaded locally to `vendor/`
|
||||
- All js files are imported with a combination of "pinning" with `config/importmaps.rb` and `app/assets/javascript/application.js` and imported to `app/views/layouts/application.html.erb`
|
||||
- Jquery, bootstrap, datatables have been downloaded locally to `vendor/`
|
||||
- Turbo and action cable are gems and get pathed properly by propshaft.
|
||||
- development is "nobuild" with `config.assets.build_assets = false` in `config/environments/development.rb`
|
||||
- production needs to run rake assets:precompile. This is done in the `deploy/rails-prod-Dockerfile`.
|
||||
|
||||
## Stimulus Implementation
|
||||
|
||||
The application has been migrated from using vanilla JavaScript to Hotwired Stimulus. The Stimulus controllers are organized in:
|
||||
|
||||
- `app/assets/javascripts/controllers/` - Contains all Stimulus controllers
|
||||
- `app/assets/javascripts/application.js` - Registers and loads all controllers
|
||||
|
||||
The importmap configuration in `config/importmap.rb` handles the loading of all JavaScript dependencies including Stimulus controllers.
|
||||
|
||||
# Using Repomix with LLMs
|
||||
`npx repomix app test`
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
//= link_tree ../images
|
||||
//= link_directory ../javascripts .js
|
||||
//= link_directory ../stylesheets .css
|
||||
|
||||
// Link all .js files from app/javascript for importmap, and vendor/javascript if needed directly
|
||||
//= link_tree ../../javascript .js
|
||||
//= link_tree ../../../vendor/assets/javascripts .js
|
||||
|
||||
// Explicitly link all .css files from app/assets/stylesheets and vendor/assets/stylesheets
|
||||
//= link_tree ../stylesheets .css
|
||||
//= link_tree ../../../vendor/assets/stylesheets .css
|
||||
|
||||
//= link_tree ../../../vendor/assets/webfonts
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
@@ -1,3 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
@@ -1,16 +1,48 @@
|
||||
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
||||
// listed below.
|
||||
//
|
||||
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
||||
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
||||
//
|
||||
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
||||
// compiled file.
|
||||
//
|
||||
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
|
||||
// about supported directives.
|
||||
//
|
||||
//= require_tree .
|
||||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
// Entry point for your JavaScript application
|
||||
|
||||
// These are pinned in config/importmap.rb
|
||||
import "@hotwired/turbo-rails";
|
||||
import { createConsumer } from "@rails/actioncable"; // Import createConsumer directly
|
||||
import "jquery";
|
||||
import "bootstrap";
|
||||
|
||||
// Stimulus setup
|
||||
import { Application } from "@hotwired/stimulus";
|
||||
|
||||
// Initialize Stimulus application
|
||||
const application = Application.start();
|
||||
window.Stimulus = application;
|
||||
|
||||
// Load all controllers from app/assets/javascripts/controllers
|
||||
// Import controllers manually
|
||||
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() {
|
||||
try {
|
||||
window.App || (window.App = {});
|
||||
window.App.cable = createConsumer(); // Use the imported createConsumer
|
||||
console.log('Action Cable Consumer Created via app/assets/javascripts/application.js');
|
||||
} catch (e) {
|
||||
console.error('Error creating ActionCable consumer:', e);
|
||||
console.error('ActionCable not loaded or createConsumer failed, App.cable not created.');
|
||||
}
|
||||
}).call(this);
|
||||
|
||||
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";
|
||||
|
||||
420
app/assets/javascripts/controllers/match_data_controller.js
Normal file
420
app/assets/javascripts/controllers/match_data_controller.js
Normal file
@@ -0,0 +1,420 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"w1Stat", "w2Stat", "statusIndicator"
|
||||
]
|
||||
|
||||
static values = {
|
||||
tournamentId: Number,
|
||||
boutNumber: Number,
|
||||
matchId: Number
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("Match data controller connected")
|
||||
this.isConnected = false
|
||||
this.pendingLocalSync = { w1: false, w2: false }
|
||||
|
||||
this.w1 = {
|
||||
name: "w1",
|
||||
stats: "",
|
||||
updated_at: null,
|
||||
timers: {
|
||||
"injury": { time: 0, startTime: null, interval: null },
|
||||
"blood": { time: 0, startTime: null, interval: null }
|
||||
}
|
||||
}
|
||||
|
||||
this.w2 = {
|
||||
name: "w2",
|
||||
stats: "",
|
||||
updated_at: null,
|
||||
timers: {
|
||||
"injury": { time: 0, startTime: null, interval: null },
|
||||
"blood": { time: 0, startTime: null, interval: null }
|
||||
}
|
||||
}
|
||||
|
||||
// Initial values
|
||||
this.updateJsValues()
|
||||
|
||||
// Set up debounced handlers for text areas
|
||||
this.debouncedW1Handler = this.debounce((el) => this.handleTextAreaInput(el, this.w1), 400)
|
||||
this.debouncedW2Handler = this.debounce((el) => this.handleTextAreaInput(el, this.w2), 400)
|
||||
|
||||
// Set up text area event listeners
|
||||
this.w1StatTarget.addEventListener('input', (event) => this.debouncedW1Handler(event.target))
|
||||
this.w2StatTarget.addEventListener('input', (event) => this.debouncedW2Handler(event.target))
|
||||
|
||||
// Initialize from local storage
|
||||
this.initializeFromLocalStorage()
|
||||
|
||||
// Setup ActionCable
|
||||
if (this.matchIdValue) {
|
||||
this.setupSubscription(this.matchIdValue)
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.cleanupSubscription()
|
||||
}
|
||||
|
||||
// Match stats core functionality
|
||||
updateStats(wrestler, text) {
|
||||
if (!wrestler) {
|
||||
console.error("updateStats called with undefined wrestler")
|
||||
return
|
||||
}
|
||||
|
||||
wrestler.stats += text + " "
|
||||
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) {
|
||||
let payload = {}
|
||||
if (wrestler.name === 'w1') payload.new_w1_stat = wrestler.stats
|
||||
else if (wrestler.name === 'w2') payload.new_w2_stat = wrestler.stats
|
||||
|
||||
if (Object.keys(payload).length > 0) {
|
||||
console.log('[ActionCable] updateStats performing send_stat:', payload)
|
||||
this.matchSubscription.perform('send_stat', payload)
|
||||
}
|
||||
} else {
|
||||
console.warn('[ActionCable] updateStats called but matchSubscription is null.')
|
||||
}
|
||||
}
|
||||
|
||||
// Specific methods for updating each wrestler
|
||||
updateW1Stats(event) {
|
||||
const text = event.currentTarget.dataset.matchDataText || ''
|
||||
this.updateStats(this.w1, text)
|
||||
}
|
||||
|
||||
updateW2Stats(event) {
|
||||
const text = event.currentTarget.dataset.matchDataText || ''
|
||||
this.updateStats(this.w2, text)
|
||||
}
|
||||
|
||||
// End period action
|
||||
endPeriod() {
|
||||
this.updateStats(this.w1, '|End Period|')
|
||||
this.updateStats(this.w2, '|End Period|')
|
||||
}
|
||||
|
||||
handleTextAreaInput(textAreaElement, wrestler) {
|
||||
const newValue = textAreaElement.value
|
||||
console.log(`Text area input detected for ${wrestler.name}:`, newValue.substring(0, 50) + "...")
|
||||
|
||||
// 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)
|
||||
|
||||
// Send the update via Action Cable if subscribed
|
||||
if (this.matchSubscription) {
|
||||
let payload = {}
|
||||
if (wrestler.name === 'w1') {
|
||||
payload.new_w1_stat = wrestler.stats
|
||||
} else if (wrestler.name === 'w2') {
|
||||
payload.new_w2_stat = wrestler.stats
|
||||
}
|
||||
if (Object.keys(payload).length > 0) {
|
||||
console.log('[ActionCable] Performing send_stat from textarea with payload:', payload)
|
||||
this.matchSubscription.perform('send_stat', payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer functions
|
||||
startTimer(wrestler, timerKey) {
|
||||
const timer = wrestler.timers[timerKey]
|
||||
if (timer.interval) return // Prevent multiple intervals
|
||||
|
||||
timer.startTime = Date.now()
|
||||
timer.interval = setInterval(() => {
|
||||
const elapsedSeconds = Math.floor((Date.now() - timer.startTime) / 1000)
|
||||
this.updateTimerDisplay(wrestler, timerKey, timer.time + elapsedSeconds)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
stopTimer(wrestler, timerKey) {
|
||||
const timer = wrestler.timers[timerKey]
|
||||
if (!timer.interval || !timer.startTime) return
|
||||
|
||||
clearInterval(timer.interval)
|
||||
const elapsedSeconds = Math.floor((Date.now() - timer.startTime) / 1000)
|
||||
timer.time += elapsedSeconds
|
||||
timer.interval = null
|
||||
timer.startTime = null
|
||||
|
||||
this.saveToLocalStorage(wrestler)
|
||||
this.updateTimerDisplay(wrestler, timerKey, timer.time)
|
||||
this.updateStatsBox(wrestler, timerKey, elapsedSeconds)
|
||||
}
|
||||
|
||||
resetTimer(wrestler, timerKey) {
|
||||
const timer = wrestler.timers[timerKey]
|
||||
this.stopTimer(wrestler, timerKey)
|
||||
timer.time = 0
|
||||
this.updateTimerDisplay(wrestler, timerKey, 0)
|
||||
this.saveToLocalStorage(wrestler)
|
||||
}
|
||||
|
||||
// Timer control methods for W1
|
||||
startW1InjuryTimer() {
|
||||
this.startTimer(this.w1, 'injury')
|
||||
}
|
||||
|
||||
stopW1InjuryTimer() {
|
||||
this.stopTimer(this.w1, 'injury')
|
||||
}
|
||||
|
||||
resetW1InjuryTimer() {
|
||||
this.resetTimer(this.w1, 'injury')
|
||||
}
|
||||
|
||||
startW1BloodTimer() {
|
||||
this.startTimer(this.w1, 'blood')
|
||||
}
|
||||
|
||||
stopW1BloodTimer() {
|
||||
this.stopTimer(this.w1, 'blood')
|
||||
}
|
||||
|
||||
resetW1BloodTimer() {
|
||||
this.resetTimer(this.w1, 'blood')
|
||||
}
|
||||
|
||||
// Timer control methods for W2
|
||||
startW2InjuryTimer() {
|
||||
this.startTimer(this.w2, 'injury')
|
||||
}
|
||||
|
||||
stopW2InjuryTimer() {
|
||||
this.stopTimer(this.w2, 'injury')
|
||||
}
|
||||
|
||||
resetW2InjuryTimer() {
|
||||
this.resetTimer(this.w2, 'injury')
|
||||
}
|
||||
|
||||
startW2BloodTimer() {
|
||||
this.startTimer(this.w2, 'blood')
|
||||
}
|
||||
|
||||
stopW2BloodTimer() {
|
||||
this.stopTimer(this.w2, 'blood')
|
||||
}
|
||||
|
||||
resetW2BloodTimer() {
|
||||
this.resetTimer(this.w2, 'blood')
|
||||
}
|
||||
|
||||
updateTimerDisplay(wrestler, timerKey, totalTime) {
|
||||
const elementId = `${wrestler.name}-${timerKey}-time`
|
||||
const element = document.getElementById(elementId)
|
||||
if (element) {
|
||||
element.innerText = `${Math.floor(totalTime / 60)}m ${totalTime % 60}s`
|
||||
}
|
||||
}
|
||||
|
||||
updateStatsBox(wrestler, timerKey, elapsedSeconds) {
|
||||
const timerType = timerKey.includes("injury") ? "Injury Time" : "Blood Time"
|
||||
const formattedTime = `${Math.floor(elapsedSeconds / 60)}m ${elapsedSeconds % 60}s`
|
||||
this.updateStats(wrestler, `${timerType}: ${formattedTime}`)
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
generateKey(wrestler_name) {
|
||||
return `${wrestler_name}-${this.tournamentIdValue}-${this.boutNumberValue}`
|
||||
}
|
||||
|
||||
loadFromLocalStorage(wrestler_name) {
|
||||
const key = this.generateKey(wrestler_name)
|
||||
const data = localStorage.getItem(key)
|
||||
return data ? JSON.parse(data) : null
|
||||
}
|
||||
|
||||
saveToLocalStorage(person) {
|
||||
const key = this.generateKey(person.name)
|
||||
const data = {
|
||||
stats: person.stats,
|
||||
updated_at: person.updated_at,
|
||||
timers: person.timers
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(data))
|
||||
}
|
||||
|
||||
updateHtmlValues() {
|
||||
if (this.w1StatTarget) this.w1StatTarget.value = this.w1.stats
|
||||
if (this.w2StatTarget) this.w2StatTarget.value = this.w2.stats
|
||||
}
|
||||
|
||||
updateJsValues() {
|
||||
if (this.w1StatTarget) this.w1.stats = this.w1StatTarget.value
|
||||
if (this.w2StatTarget) this.w2.stats = this.w2StatTarget.value
|
||||
}
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout
|
||||
return (...args) => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
|
||||
initializeTimers(wrestler) {
|
||||
for (const timerKey in wrestler.timers) {
|
||||
this.updateTimerDisplay(wrestler, timerKey, wrestler.timers[timerKey].time)
|
||||
}
|
||||
}
|
||||
|
||||
initializeFromLocalStorage() {
|
||||
const w1Data = this.loadFromLocalStorage('w1')
|
||||
if (w1Data) {
|
||||
this.w1.stats = w1Data.stats || ''
|
||||
this.w1.updated_at = w1Data.updated_at
|
||||
if (w1Data.timers) this.w1.timers = w1Data.timers
|
||||
this.initializeTimers(this.w1)
|
||||
}
|
||||
|
||||
const w2Data = this.loadFromLocalStorage('w2')
|
||||
if (w2Data) {
|
||||
this.w2.stats = w2Data.stats || ''
|
||||
this.w2.updated_at = w2Data.updated_at
|
||||
if (w2Data.timers) this.w2.timers = w2Data.timers
|
||||
this.initializeTimers(this.w2)
|
||||
}
|
||||
|
||||
this.updateHtmlValues()
|
||||
}
|
||||
|
||||
cleanupSubscription() {
|
||||
if (this.matchSubscription) {
|
||||
console.log(`[Stats AC Cleanup] Unsubscribing from match channel.`)
|
||||
try {
|
||||
this.matchSubscription.unsubscribe()
|
||||
} catch (e) {
|
||||
console.error(`[Stats AC Cleanup] Error during unsubscribe:`, e)
|
||||
}
|
||||
this.matchSubscription = null
|
||||
}
|
||||
}
|
||||
|
||||
setupSubscription(matchId) {
|
||||
this.cleanupSubscription()
|
||||
console.log(`[Stats AC Setup] Attempting subscription for match ID: ${matchId}`)
|
||||
|
||||
// Update status indicator
|
||||
if (this.statusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.innerText = "Connecting to server for real-time stat updates..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-warning', 'alert-danger')
|
||||
this.statusIndicatorTarget.classList.add('alert-info')
|
||||
}
|
||||
|
||||
// Exit if we don't have App.cable
|
||||
if (!window.App || !window.App.cable) {
|
||||
console.error(`[Stats AC Setup] Error: App.cable is not available.`)
|
||||
if (this.statusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.innerText = "Error: WebSockets unavailable. Stats won't update in real-time."
|
||||
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-warning')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.matchSubscription = App.cable.subscriptions.create(
|
||||
{
|
||||
channel: "MatchChannel",
|
||||
match_id: matchId
|
||||
},
|
||||
{
|
||||
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')
|
||||
this.statusIndicatorTarget.classList.add('alert-warning')
|
||||
}
|
||||
},
|
||||
|
||||
received: (data) => {
|
||||
console.log(`[Stats AC] Received data:`, data)
|
||||
|
||||
// Update w1 stats
|
||||
if (data.w1_stat !== undefined && this.w1StatTarget) {
|
||||
console.log(`[Stats AC] Updating w1_stat: ${data.w1_stat.substring(0, 30)}...`)
|
||||
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)}...`)
|
||||
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.')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
receive_error: (error) => {
|
||||
console.error(`[Stats AC] Error:`, error)
|
||||
this.matchSubscription = null
|
||||
|
||||
if (this.statusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.innerText = "Error: Connection issue. Stats won't update in real-time."
|
||||
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-warning')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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.')
|
||||
}
|
||||
}
|
||||
}
|
||||
237
app/assets/javascripts/controllers/match_score_controller.js
Normal file
237
app/assets/javascripts/controllers/match_score_controller.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"winType", "winnerSelect", "submitButton", "dynamicScoreInput",
|
||||
"finalScoreField", "validationAlerts", "pinTimeTip"
|
||||
]
|
||||
|
||||
static values = {
|
||||
winnerScore: { type: String, default: "0" },
|
||||
loserScore: { type: String, default: "0" }
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("Match score controller connected")
|
||||
// Use setTimeout to ensure the DOM is fully loaded
|
||||
setTimeout(() => {
|
||||
this.updateScoreInput()
|
||||
this.validateForm()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
winTypeChanged() {
|
||||
this.updateScoreInput()
|
||||
this.validateForm()
|
||||
}
|
||||
|
||||
winnerChanged() {
|
||||
this.validateForm()
|
||||
}
|
||||
|
||||
updateScoreInput() {
|
||||
const winType = this.winTypeTarget.value
|
||||
this.dynamicScoreInputTarget.innerHTML = ""
|
||||
|
||||
// Add section header
|
||||
const header = document.createElement("h5")
|
||||
header.innerText = `Score Input for ${winType}`
|
||||
header.classList.add("mt-2", "mb-3")
|
||||
this.dynamicScoreInputTarget.appendChild(header)
|
||||
|
||||
if (winType === "Pin") {
|
||||
this.pinTimeTipTarget.style.display = "block"
|
||||
|
||||
const minuteInput = this.createTextInput("minutes", "Minutes (MM)", "Pin Time Minutes")
|
||||
const secondInput = this.createTextInput("seconds", "Seconds (SS)", "Pin Time Seconds")
|
||||
|
||||
this.dynamicScoreInputTarget.appendChild(minuteInput)
|
||||
this.dynamicScoreInputTarget.appendChild(secondInput)
|
||||
|
||||
// Add event listeners to the new inputs
|
||||
const inputs = this.dynamicScoreInputTarget.querySelectorAll("input")
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener("input", () => {
|
||||
this.updatePinTimeScore()
|
||||
this.validateForm()
|
||||
})
|
||||
})
|
||||
|
||||
this.updatePinTimeScore()
|
||||
} else if (["Decision", "Major", "Tech Fall"].includes(winType)) {
|
||||
this.pinTimeTipTarget.style.display = "none"
|
||||
|
||||
const winnerScoreInput = this.createTextInput(
|
||||
"winner-score",
|
||||
"Winner's Score",
|
||||
"Enter the winner's score"
|
||||
)
|
||||
const loserScoreInput = this.createTextInput(
|
||||
"loser-score",
|
||||
"Loser's Score",
|
||||
"Enter the loser's score"
|
||||
)
|
||||
|
||||
this.dynamicScoreInputTarget.appendChild(winnerScoreInput)
|
||||
this.dynamicScoreInputTarget.appendChild(loserScoreInput)
|
||||
|
||||
// Restore stored values
|
||||
const winnerInput = winnerScoreInput.querySelector("input")
|
||||
const loserInput = loserScoreInput.querySelector("input")
|
||||
|
||||
winnerInput.value = this.winnerScoreValue
|
||||
loserInput.value = this.loserScoreValue
|
||||
|
||||
// Add event listeners to the new inputs
|
||||
winnerInput.addEventListener("input", (event) => {
|
||||
this.winnerScoreValue = event.target.value || "0"
|
||||
this.updatePointScore()
|
||||
this.validateForm()
|
||||
})
|
||||
|
||||
loserInput.addEventListener("input", (event) => {
|
||||
this.loserScoreValue = event.target.value || "0"
|
||||
this.updatePointScore()
|
||||
this.validateForm()
|
||||
})
|
||||
|
||||
this.updatePointScore()
|
||||
} else {
|
||||
// For other types (forfeit, etc.), clear the input and hide pin time tip
|
||||
this.pinTimeTipTarget.style.display = "none"
|
||||
this.finalScoreFieldTarget.value = ""
|
||||
|
||||
// Show message for non-score win types
|
||||
const message = document.createElement("p")
|
||||
message.innerText = `No score required for ${winType} win type.`
|
||||
message.classList.add("text-muted")
|
||||
this.dynamicScoreInputTarget.appendChild(message)
|
||||
}
|
||||
|
||||
this.validateForm()
|
||||
}
|
||||
|
||||
updatePinTimeScore() {
|
||||
const minuteInput = this.dynamicScoreInputTarget.querySelector("#minutes")
|
||||
const secondInput = this.dynamicScoreInputTarget.querySelector("#seconds")
|
||||
|
||||
if (minuteInput && secondInput) {
|
||||
const minutes = (minuteInput.value || "0").padStart(2, "0")
|
||||
const seconds = (secondInput.value || "0").padStart(2, "0")
|
||||
this.finalScoreFieldTarget.value = `${minutes}:${seconds}`
|
||||
|
||||
// Validate after updating pin time
|
||||
this.validateForm()
|
||||
}
|
||||
}
|
||||
|
||||
updatePointScore() {
|
||||
const winnerScore = this.winnerScoreValue || "0"
|
||||
const loserScore = this.loserScoreValue || "0"
|
||||
this.finalScoreFieldTarget.value = `${winnerScore}-${loserScore}`
|
||||
|
||||
// Validate immediately after updating scores
|
||||
this.validateForm()
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
const winType = this.winTypeTarget.value
|
||||
const winner = this.winnerSelectTarget?.value
|
||||
let isValid = true
|
||||
let alertMessage = ""
|
||||
let winTypeShouldBe = "Decision"
|
||||
|
||||
// Clear previous validation messages
|
||||
this.validationAlertsTarget.innerHTML = ""
|
||||
this.validationAlertsTarget.style.display = "none"
|
||||
this.validationAlertsTarget.classList.remove("alert", "alert-danger", "p-3")
|
||||
|
||||
if (["Decision", "Major", "Tech Fall"].includes(winType)) {
|
||||
// Get scores and ensure they're valid numbers
|
||||
const winnerScore = parseInt(this.winnerScoreValue || "0", 10)
|
||||
const loserScore = parseInt(this.loserScoreValue || "0", 10)
|
||||
|
||||
console.log(`Validating scores: winner=${winnerScore}, loser=${loserScore}, type=${winType}`)
|
||||
|
||||
// Check if winner score > loser score
|
||||
if (winnerScore <= loserScore) {
|
||||
isValid = false
|
||||
alertMessage += "<strong>Error:</strong> Winner's score must be higher than loser's score.<br>"
|
||||
} else {
|
||||
// Calculate score difference and determine correct win type
|
||||
const scoreDifference = winnerScore - loserScore
|
||||
|
||||
if (scoreDifference < 8) {
|
||||
winTypeShouldBe = "Decision"
|
||||
} else if (scoreDifference >= 8 && scoreDifference < 15) {
|
||||
winTypeShouldBe = "Major"
|
||||
} else if (scoreDifference >= 15) {
|
||||
winTypeShouldBe = "Tech Fall"
|
||||
}
|
||||
|
||||
// Check if selected win type matches the correct one based on score difference
|
||||
if (winTypeShouldBe !== winType) {
|
||||
isValid = false
|
||||
alertMessage += `
|
||||
<strong>Win Type Error:</strong> Win type should be <strong>${winTypeShouldBe}</strong>.<br>
|
||||
<ul>
|
||||
<li>Decisions are wins with a score difference less than 8.</li>
|
||||
<li>Majors are wins with a score difference between 8 and 14.</li>
|
||||
<li>Tech Falls are wins with a score difference of 15 or more.</li>
|
||||
</ul>
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a winner is selected
|
||||
if (!winner) {
|
||||
isValid = false
|
||||
alertMessage += "<strong>Error:</strong> Please select a winner.<br>"
|
||||
}
|
||||
|
||||
// Display validation messages if any
|
||||
if (alertMessage) {
|
||||
this.validationAlertsTarget.innerHTML = alertMessage
|
||||
this.validationAlertsTarget.style.display = "block"
|
||||
this.validationAlertsTarget.classList.add("alert", "alert-danger", "p-3")
|
||||
}
|
||||
|
||||
// Enable/disable submit button based on validation result
|
||||
this.submitButtonTarget.disabled = !isValid
|
||||
|
||||
// Return validation result for potential use elsewhere
|
||||
return isValid
|
||||
}
|
||||
|
||||
createTextInput(id, placeholder, label) {
|
||||
const container = document.createElement("div")
|
||||
container.classList.add("form-group", "mb-2")
|
||||
|
||||
const inputLabel = document.createElement("label")
|
||||
inputLabel.innerText = label
|
||||
inputLabel.classList.add("form-label")
|
||||
inputLabel.setAttribute("for", id)
|
||||
|
||||
const input = document.createElement("input")
|
||||
input.type = "text"
|
||||
input.id = id
|
||||
input.placeholder = placeholder
|
||||
input.classList.add("form-control")
|
||||
input.style.width = "100%"
|
||||
input.style.maxWidth = "400px"
|
||||
|
||||
container.appendChild(inputLabel)
|
||||
container.appendChild(input)
|
||||
return container
|
||||
}
|
||||
|
||||
confirmWinner(event) {
|
||||
const winnerSelect = this.winnerSelectTarget;
|
||||
const selectedOption = winnerSelect.options[winnerSelect.selectedIndex];
|
||||
|
||||
if (!confirm('Is the name of the winner ' + selectedOption.text + '?')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
139
app/assets/javascripts/controllers/match_spectate_controller.js
Normal file
139
app/assets/javascripts/controllers/match_spectate_controller.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"w1Stats", "w2Stats", "winner", "winType",
|
||||
"score", "finished", "statusIndicator"
|
||||
]
|
||||
|
||||
static values = {
|
||||
matchId: Number
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("Match spectate controller connected")
|
||||
|
||||
// Setup ActionCable connection if match ID is available
|
||||
if (this.matchIdValue) {
|
||||
this.setupSubscription(this.matchIdValue)
|
||||
} else {
|
||||
console.warn("No match ID provided for spectate controller")
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.cleanupSubscription()
|
||||
}
|
||||
|
||||
// Clean up the existing subscription
|
||||
cleanupSubscription() {
|
||||
if (this.matchSubscription) {
|
||||
console.log('[Spectator AC Cleanup] Unsubscribing...')
|
||||
this.matchSubscription.unsubscribe()
|
||||
this.matchSubscription = null
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the Action Cable subscription for a given matchId
|
||||
setupSubscription(matchId) {
|
||||
this.cleanupSubscription() // Ensure clean state
|
||||
console.log(`[Spectator AC Setup] Attempting subscription for match ID: ${matchId}`)
|
||||
|
||||
if (typeof App === 'undefined' || typeof App.cable === 'undefined') {
|
||||
console.error("[Spectator AC Setup] Action Cable consumer not found.")
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Error: AC Not Loaded"
|
||||
this.statusIndicatorTarget.classList.remove('text-dark', 'text-success')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger', 'text-danger')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Set initial connecting state for indicator
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Connecting to backend for live updates..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success')
|
||||
this.statusIndicatorTarget.classList.add('alert-secondary', 'text-dark')
|
||||
}
|
||||
|
||||
// Assign to the instance property
|
||||
this.matchSubscription = App.cable.subscriptions.create(
|
||||
{ channel: "MatchChannel", match_id: matchId },
|
||||
{
|
||||
initialized: () => {
|
||||
console.log(`[Spectator AC Callback] Initialized: ${matchId}`)
|
||||
// Set connecting state again in case of retry
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Connecting to backend for live updates..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success')
|
||||
this.statusIndicatorTarget.classList.add('alert-secondary', 'text-dark')
|
||||
}
|
||||
},
|
||||
connected: () => {
|
||||
console.log(`[Spectator AC Callback] CONNECTED: ${matchId}`)
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Connected to backend for live updates..."
|
||||
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}`)
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Disconnected from backend for live updates. Retrying..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger')
|
||||
}
|
||||
},
|
||||
rejected: () => {
|
||||
console.error(`[Spectator AC Callback] REJECTED: ${matchId}`)
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Connection to backend rejected"
|
||||
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger')
|
||||
}
|
||||
this.matchSubscription = null
|
||||
},
|
||||
received: (data) => {
|
||||
console.log("[Spectator AC Callback] Received:", data)
|
||||
this.updateDisplayElements(data)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Update UI elements with received data
|
||||
updateDisplayElements(data) {
|
||||
// Update display elements if they exist and data is provided
|
||||
if (data.w1_stat !== undefined && this.hasW1StatsTarget) {
|
||||
this.w1StatsTarget.textContent = data.w1_stat
|
||||
}
|
||||
|
||||
if (data.w2_stat !== undefined && this.hasW2StatsTarget) {
|
||||
this.w2StatsTarget.textContent = data.w2_stat
|
||||
}
|
||||
|
||||
if (data.score !== undefined && this.hasScoreTarget) {
|
||||
this.scoreTarget.textContent = data.score || '-'
|
||||
}
|
||||
|
||||
if (data.win_type !== undefined && this.hasWinTypeTarget) {
|
||||
this.winTypeTarget.textContent = data.win_type || '-'
|
||||
}
|
||||
|
||||
if (data.winner_name !== undefined && this.hasWinnerTarget) {
|
||||
this.winnerTarget.textContent = data.winner_name || (data.winner_id ? `ID: ${data.winner_id}` : '-')
|
||||
} else if (data.winner_id !== undefined && this.hasWinnerTarget) {
|
||||
this.winnerTarget.textContent = data.winner_id ? `ID: ${data.winner_id}` : '-'
|
||||
}
|
||||
|
||||
if (data.finished !== undefined && this.hasFinishedTarget) {
|
||||
this.finishedTarget.textContent = data.finished ? 'Yes' : 'No'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"w1Takedown", "w1Escape", "w1Reversal", "w1Nf2", "w1Nf3", "w1Nf4", "w1Nf5", "w1Penalty", "w1Penalty2",
|
||||
"w1Top", "w1Bottom", "w1Neutral", "w1Defer", "w1Stalling", "w1Caution", "w1ColorSelect",
|
||||
"w2Takedown", "w2Escape", "w2Reversal", "w2Nf2", "w2Nf3", "w2Nf4", "w2Nf5", "w2Penalty", "w2Penalty2",
|
||||
"w2Top", "w2Bottom", "w2Neutral", "w2Defer", "w2Stalling", "w2Caution", "w2ColorSelect"
|
||||
]
|
||||
|
||||
connect() {
|
||||
console.log("Wrestler color controller connected")
|
||||
this.initializeColors()
|
||||
}
|
||||
|
||||
initializeColors() {
|
||||
// Set initial colors based on select values
|
||||
this.changeW1Color({ preventRecursion: true })
|
||||
}
|
||||
|
||||
changeW1Color(options = {}) {
|
||||
const color = this.w1ColorSelectTarget.value
|
||||
this.setElementsColor("w1", color)
|
||||
|
||||
// Update w2 color to the opposite color unless we're already in a recursive call
|
||||
if (!options.preventRecursion) {
|
||||
const oppositeColor = color === "green" ? "red" : "green"
|
||||
this.w2ColorSelectTarget.value = oppositeColor
|
||||
this.setElementsColor("w2", oppositeColor)
|
||||
}
|
||||
}
|
||||
|
||||
changeW2Color(options = {}) {
|
||||
const color = this.w2ColorSelectTarget.value
|
||||
this.setElementsColor("w2", color)
|
||||
|
||||
// Update w1 color to the opposite color unless we're already in a recursive call
|
||||
if (!options.preventRecursion) {
|
||||
const oppositeColor = color === "green" ? "red" : "green"
|
||||
this.w1ColorSelectTarget.value = oppositeColor
|
||||
this.setElementsColor("w1", oppositeColor)
|
||||
}
|
||||
}
|
||||
|
||||
setElementsColor(wrestler, color) {
|
||||
// Define which targets to update for each wrestler
|
||||
const targetSuffixes = [
|
||||
"Takedown", "Escape", "Reversal", "Nf2", "Nf3", "Nf4", "Nf5", "Penalty", "Penalty2",
|
||||
"Top", "Bottom", "Neutral", "Defer", "Stalling", "Caution"
|
||||
]
|
||||
|
||||
// For each target type, update the class
|
||||
targetSuffixes.forEach(suffix => {
|
||||
const targetName = `${wrestler}${suffix}Target`
|
||||
if (this[targetName]) {
|
||||
// Remove existing color classes
|
||||
this[targetName].classList.remove("btn-success", "btn-danger")
|
||||
|
||||
// Add new color class
|
||||
if (color === "green") {
|
||||
this[targetName].classList.add("btn-success")
|
||||
} else if (color === "red") {
|
||||
this[targetName].classList.add("btn-danger")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
@@ -1,3 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
@@ -1,3 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
@@ -1,3 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
@@ -1,3 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
@@ -1,3 +0,0 @@
|
||||
# Place all the behaviors and hooks related to the matching controller here.
|
||||
# All this logic will automatically be available in application.js.
|
||||
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||
@@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the Admin controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@@ -1,4 +0,0 @@
|
||||
/*
|
||||
Place all the styles related to the matching controller here.
|
||||
They will automatically be included in application.css.
|
||||
*/
|
||||
@@ -1,15 +1,17 @@
|
||||
/*
|
||||
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
||||
* listed below.
|
||||
*
|
||||
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
||||
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
||||
*
|
||||
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
||||
* compiled file so the styles you add here take precedence over styles defined in any styles
|
||||
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
|
||||
* file per style scope.
|
||||
*
|
||||
*= require_tree .
|
||||
*= require_self
|
||||
*/
|
||||
/* relative pathing from /vender/assets/stylesheets = / */
|
||||
@import url("/bootstrap.min.css");
|
||||
@import url("/bootstrap-theme.min.css");
|
||||
@import url("/fontawesome/all.css");
|
||||
@import url("/custom.css");
|
||||
@import url("/scaffolds.css");
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Brands';
|
||||
/* relative pathing from /vender/assets/stylesheets = / */
|
||||
src: url("/webfonts/fa-brands-400.eot");
|
||||
src: url("/webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"),
|
||||
url("/webfonts/fa-brands-400.woff2") format("woff2"),
|
||||
url("/webfonts/fa-brands-400.woff") format("woff"),
|
||||
url("/webfonts/fa-brands-400.ttf") format("truetype"),
|
||||
url("/webfonts/fa-brands-400.svg#fontawesome") format("svg");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.navbar-inverse.navbar-fixed-top {
|
||||
z-index: 1040; /* Ensure main navbar is above tournament navbar */
|
||||
}
|
||||
|
||||
#tournament-navbar {
|
||||
top: 50px; /* Position below the first fixed navbar */
|
||||
z-index: 1030; /* Explicitly set standard fixed navbar z-index */
|
||||
}
|
||||
|
||||
/* Make desktop navbar dropdowns scrollable if they overflow */
|
||||
@media (min-width: 768px) {
|
||||
/* Target dropdowns in main nav and tournament nav specifically */
|
||||
.navbar-fixed-top .dropdown-menu {
|
||||
max-height: 70vh; /* Adjust as needed - 70% of viewport height */
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the Matches controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the Mats controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the Schools controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the StaticPages controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the tournaments controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the Weights controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
@@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the Wrestlers controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
||||
4
app/channels/application_cable/connection.rb
Normal file
4
app/channels/application_cable/connection.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
end
|
||||
88
app/channels/match_channel.rb
Normal file
88
app/channels/match_channel.rb
Normal file
@@ -0,0 +1,88 @@
|
||||
class MatchChannel < ApplicationCable::Channel
|
||||
def subscribed
|
||||
@match = Match.find_by(id: params[:match_id])
|
||||
Rails.logger.info "[MatchChannel] Client subscribed with match_id: #{params[:match_id]}. Match found: #{@match.present?}"
|
||||
if @match
|
||||
stream_for @match
|
||||
else
|
||||
Rails.logger.warn "[MatchChannel] Match not found for ID: #{params[:match_id]}. Subscription may fail."
|
||||
# You might want to reject the subscription if the match isn't found
|
||||
# reject
|
||||
end
|
||||
end
|
||||
|
||||
def unsubscribed
|
||||
Rails.logger.info "[MatchChannel] Client unsubscribed for match #{@match&.id}"
|
||||
end
|
||||
|
||||
# Called when client sends data with action: 'send_stat'
|
||||
def send_stat(data)
|
||||
# Explicit check for @match at the start
|
||||
unless @match
|
||||
Rails.logger.error "[MatchChannel] Error: send_stat called but @match is nil. Client params on sub: #{params[:match_id]}"
|
||||
return # Stop if no match context
|
||||
end
|
||||
|
||||
Rails.logger.info "[MatchChannel] Received send_stat for match #{@match.id} with data: #{data.inspect}"
|
||||
|
||||
# Prepare attributes to update
|
||||
attributes_to_update = {}
|
||||
attributes_to_update[:w1_stat] = data['new_w1_stat'] if data.key?('new_w1_stat')
|
||||
attributes_to_update[:w2_stat] = data['new_w2_stat'] if data.key?('new_w2_stat')
|
||||
|
||||
if attributes_to_update.present?
|
||||
# Persist the changes to the database
|
||||
# Note: Consider background job or throttling for very high frequency updates
|
||||
begin
|
||||
if @match.update(attributes_to_update)
|
||||
Rails.logger.info "[MatchChannel] Updated match #{@match.id} stats in DB: #{attributes_to_update.keys.join(', ')}"
|
||||
|
||||
# Prepare payload for broadcast (using potentially updated values from @match)
|
||||
payload = {
|
||||
w1_stat: @match.w1_stat,
|
||||
w2_stat: @match.w2_stat
|
||||
}.compact
|
||||
|
||||
if payload.present?
|
||||
Rails.logger.info "[MatchChannel] Broadcasting DB-persisted stats to match #{@match.id} with payload: #{payload.inspect}"
|
||||
MatchChannel.broadcast_to(@match, payload)
|
||||
else
|
||||
Rails.logger.info "[MatchChannel] Payload empty after DB update for match #{@match.id}, not broadcasting."
|
||||
end
|
||||
else
|
||||
Rails.logger.error "[MatchChannel] Failed to update match #{@match.id} stats in DB: #{@match.errors.full_messages.join(', ')}"
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "[MatchChannel] Exception during match update for #{@match.id}: #{e.message}"
|
||||
Rails.logger.error e.backtrace.join("\n")
|
||||
end
|
||||
else
|
||||
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
|
||||
@@ -14,7 +14,11 @@ class ApiController < ApplicationController
|
||||
end
|
||||
|
||||
def tournament
|
||||
@tournament = Tournament.where(:id => params[:tournament]).includes(:schools,:weights,:mats,:matches,:user,:wrestlers).first
|
||||
@tournament = Tournament.where(:id => params[:tournament]).includes(:user, :mats, :schools, :weights, :matches, wrestlers: [:school, :weight, :matches_as_w1, :matches_as_w2]).first
|
||||
@schools = @tournament.schools.includes(wrestlers: [:weight, :matches_as_w1, :matches_as_w2])
|
||||
@weights = @tournament.weights.includes(wrestlers: [:school, :matches_as_w1, :matches_as_w2])
|
||||
@matches = @tournament.matches.includes(:wrestlers,:schools)
|
||||
@mats = @tournament.mats.includes(:matches)
|
||||
end
|
||||
|
||||
def newTournament
|
||||
|
||||
@@ -5,19 +5,39 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
after_action :set_csrf_cookie_for_ng
|
||||
|
||||
# Add helpers for authentication (replacing Devise)
|
||||
helper_method :current_user, :user_signed_in?
|
||||
|
||||
def current_user
|
||||
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
|
||||
end
|
||||
|
||||
def user_signed_in?
|
||||
current_user.present?
|
||||
end
|
||||
|
||||
def authenticate_user!
|
||||
redirect_to login_path, alert: "Please log in to access this page" unless user_signed_in?
|
||||
end
|
||||
|
||||
def set_csrf_cookie_for_ng
|
||||
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
|
||||
end
|
||||
|
||||
rescue_from CanCan::AccessDenied do |exception|
|
||||
# flash[:error] = "Access denied!"
|
||||
redirect_to '/static_pages/not_allowed'
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
# In Rails 4.2 and above
|
||||
def verified_request?
|
||||
super || valid_authenticity_token?(session, request.headers['X-XSRF-TOKEN'])
|
||||
end
|
||||
|
||||
# Override current_ability to pass school_permission_key
|
||||
# @school_permission_key needs to be defined on the controller
|
||||
def current_ability
|
||||
@current_ability ||= Ability.new(current_user, @school_permission_key)
|
||||
end
|
||||
end
|
||||
|
||||
77
app/controllers/mat_assignment_rules_controller.rb
Normal file
77
app/controllers/mat_assignment_rules_controller.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
class MatAssignmentRulesController < ApplicationController
|
||||
before_action :set_tournament
|
||||
before_action :check_access_manage
|
||||
before_action :set_mat_assignment_rule, only: [:edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@mat_assignment_rules = @tournament.mat_assignment_rules.includes(:mat)
|
||||
@weights_by_id = @tournament.weights.index_by(&:id) # For quick lookup
|
||||
end
|
||||
|
||||
def new
|
||||
@mat_assignment_rule = @tournament.mat_assignment_rules.build
|
||||
load_form_data
|
||||
end
|
||||
|
||||
def create
|
||||
@mat_assignment_rule = @tournament.mat_assignment_rules.build(mat_assignment_rule_params)
|
||||
load_form_data
|
||||
|
||||
if @mat_assignment_rule.save
|
||||
redirect_to tournament_mat_assignment_rules_path(@tournament), notice: 'Mat assignment rule created successfully.'
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
load_form_data
|
||||
end
|
||||
|
||||
def update
|
||||
load_form_data
|
||||
|
||||
if @mat_assignment_rule.update(mat_assignment_rule_params)
|
||||
redirect_to tournament_mat_assignment_rules_path(@tournament), notice: 'Mat assignment rule updated successfully.'
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@mat_assignment_rule.destroy
|
||||
redirect_to tournament_mat_assignment_rules_path(@tournament), notice: 'Mat assignment rule was successfully deleted.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:tournament_id])
|
||||
end
|
||||
|
||||
def set_mat_assignment_rule
|
||||
@mat_assignment_rule = @tournament.mat_assignment_rules.find(params[:id])
|
||||
end
|
||||
|
||||
def check_access_manage
|
||||
authorize! :manage, @tournament
|
||||
end
|
||||
|
||||
def mat_assignment_rule_params
|
||||
params[:mat_assignment_rule][:weight_classes] ||= []
|
||||
params[:mat_assignment_rule][:bracket_positions] ||= []
|
||||
params[:mat_assignment_rule][:rounds] ||= []
|
||||
|
||||
params.require(:mat_assignment_rule).permit(:mat_id, weight_classes: [], bracket_positions: [], rounds: []).tap do |whitelisted|
|
||||
whitelisted[:weight_classes] = Array(whitelisted[:weight_classes]).map(&:to_i)
|
||||
whitelisted[:rounds] = Array(whitelisted[:rounds]).map(&:to_i)
|
||||
whitelisted[:bracket_positions] = Array(whitelisted[:bracket_positions])
|
||||
end
|
||||
end
|
||||
|
||||
def load_form_data
|
||||
@available_mats = @tournament.mats
|
||||
@unique_bracket_positions = @tournament.matches.select(:bracket_position).distinct.pluck(:bracket_position)
|
||||
@unique_rounds = @tournament.matches.select(:round).distinct.pluck(:round)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
class MatchesController < ApplicationController
|
||||
before_action :set_match, only: [:show, :edit, :update, :destroy, :stat]
|
||||
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
|
||||
@@ -16,41 +16,155 @@ class MatchesController < ApplicationController
|
||||
end
|
||||
if @match
|
||||
@wrestlers = @match.weight.wrestlers
|
||||
@tournament = @match.tournament
|
||||
end
|
||||
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
|
||||
end
|
||||
@wrestlers = []
|
||||
if @match
|
||||
@w1 = @match.wrestler1
|
||||
@w2 = @match.wrestler2
|
||||
@wrestlers = [@w1,@w2]
|
||||
if @match.w1
|
||||
@wrestler1_name = @match.wrestler1.name
|
||||
@wrestler1_school_name = @match.wrestler1.school.name
|
||||
@wrestler1_last_match = @match.wrestler1.last_match
|
||||
@wrestlers.push(@match.wrestler1)
|
||||
else
|
||||
@wrestler1_name = "Not assigned"
|
||||
@wrestler1_school_name = "N/A"
|
||||
@wrestler1_last_match = nil
|
||||
end
|
||||
if @match.w2
|
||||
@wrestler2_name = @match.wrestler2.name
|
||||
@wrestler2_school_name = @match.wrestler2.school.name
|
||||
@wrestler2_last_match = @match.wrestler2.last_match
|
||||
@wrestlers.push(@match.wrestler2)
|
||||
else
|
||||
@wrestler2_name = "Not assigned"
|
||||
@wrestler2_school_name = "N/A"
|
||||
@wrestler2_last_match = nil
|
||||
end
|
||||
@tournament = @match.tournament
|
||||
end
|
||||
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
|
||||
def spectate
|
||||
# Similar to stat, but potentially simplified for read-only view
|
||||
# We mainly need @match for the view to get the ID
|
||||
# and maybe initial wrestler names/schools
|
||||
if @match
|
||||
@wrestler1_name = @match.w1 ? @match.wrestler1.name : "Not assigned"
|
||||
@wrestler1_school_name = @match.w1 ? @match.wrestler1.school.name : "N/A"
|
||||
@wrestler2_name = @match.w2 ? @match.wrestler2.name : "Not assigned"
|
||||
@wrestler2_school_name = @match.w2 ? @match.wrestler2.school.name : "N/A"
|
||||
@tournament = @match.tournament
|
||||
else
|
||||
# Handle case where match isn't found, perhaps redirect or render error
|
||||
redirect_to root_path, alert: "Match not found."
|
||||
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
|
||||
respond_to do |format|
|
||||
if @match.update(match_params)
|
||||
# Broadcast the update
|
||||
MatchChannel.broadcast_to(
|
||||
@match,
|
||||
{
|
||||
w1_stat: @match.w1_stat,
|
||||
w2_stat: @match.w2_stat,
|
||||
score: @match.score,
|
||||
win_type: @match.win_type,
|
||||
winner_id: @match.winner_id,
|
||||
winner_name: @match.winner&.name,
|
||||
finished: @match.finished
|
||||
}
|
||||
)
|
||||
|
||||
if session[:return_path]
|
||||
format.html { redirect_to session.delete(:return_path), notice: 'Match was successfully updated.' }
|
||||
sanitized_return_path = sanitize_return_path(session[:return_path])
|
||||
format.html { redirect_to sanitized_return_path, notice: 'Match was successfully updated.' }
|
||||
session.delete(:return_path) # Remove the session variable
|
||||
else
|
||||
format.html { redirect_to "/tournaments/#{@match.tournament.id}", notice: 'Match was successfully updated.' }
|
||||
end
|
||||
format.json { head :no_content }
|
||||
else
|
||||
format.html { render action: 'edit' }
|
||||
format.json { render json: @match.errors, status: :unprocessable_entity }
|
||||
if session[:error_return_path]
|
||||
format.html { redirect_to session.delete(:error_return_path), alert: "Match did not save because: #{@match.errors.full_messages.to_s}" }
|
||||
format.json { render json: @match.errors, status: :unprocessable_entity }
|
||||
else
|
||||
format.html { redirect_to "/tournaments/#{@match.tournament.id}", alert: "Match did not save because: #{@match.errors.full_messages.to_s}" }
|
||||
format.json { render json: @match.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
@@ -61,10 +175,18 @@ class MatchesController < ApplicationController
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def match_params
|
||||
params.require(:match).permit(:w1, :w2, :w1_stat, :w2_stat, :winner_id, :win_type, :score, :finished)
|
||||
params.require(:match).permit(:w1, :w2, :w1_stat, :w2_stat, :winner_id, :win_type, :score, :overtime_type, :finished, :round)
|
||||
end
|
||||
|
||||
def check_access
|
||||
authorize! :manage, @match.tournament
|
||||
end
|
||||
|
||||
def sanitize_return_path(path)
|
||||
uri = URI.parse(path)
|
||||
params = Rack::Utils.parse_nested_query(uri.query)
|
||||
params.delete("bout_number") # Remove the bout_number param
|
||||
uri.query = params.to_query.presence # Rebuild the query string or set it to nil if empty
|
||||
uri.to_s # Return the full path as a string
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,19 +1,52 @@
|
||||
class MatsController < ApplicationController
|
||||
before_action :set_mat, only: [:show, :edit, :update, :destroy]
|
||||
before_action :check_access, only: [:new,:create,:update,:destroy,:edit,:show]
|
||||
before_action :check_for_matches, only: [:show]
|
||||
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]
|
||||
|
||||
# GET /mats/1
|
||||
# GET /mats/1.json
|
||||
def show
|
||||
@match = @mat.unfinished_matches.first
|
||||
if @match
|
||||
@w1 = @match.wrestler1
|
||||
@w2 = @match.wrestler2
|
||||
@wrestlers = [@w1,@w2]
|
||||
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
|
||||
@queue_matches[0]
|
||||
end
|
||||
# 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
|
||||
if @match.w1
|
||||
@wrestler1_name = @match.wrestler1.name
|
||||
@wrestler1_school_name = @match.wrestler1.school.name
|
||||
@wrestler1_last_match = @match.wrestler1.last_match
|
||||
@wrestlers.push(@match.wrestler1)
|
||||
else
|
||||
@wrestler1_name = "Not assigned"
|
||||
@wrestler1_school_name = "N/A"
|
||||
@wrestler1_last_match = nil
|
||||
end
|
||||
|
||||
if @match.w2
|
||||
@wrestler2_name = @match.wrestler2.name
|
||||
@wrestler2_school_name = @match.wrestler2.school.name
|
||||
@wrestler2_last_match = @match.wrestler2.last_match
|
||||
@wrestlers.push(@match.wrestler2)
|
||||
else
|
||||
@wrestler2_name = "Not assigned"
|
||||
@wrestler2_school_name = "N/A"
|
||||
@wrestler2_last_match = nil
|
||||
end
|
||||
|
||||
@tournament = @match.tournament
|
||||
end
|
||||
|
||||
session[:return_path] = request.original_fullpath
|
||||
end
|
||||
session[:error_return_path] = request.original_fullpath
|
||||
end
|
||||
|
||||
# GET /mats/new
|
||||
def new
|
||||
@@ -44,6 +77,20 @@ class MatsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
# POST /mats/1/assign_next_match
|
||||
def assign_next_match
|
||||
@tournament = @mat.tournament_id
|
||||
respond_to do |format|
|
||||
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." }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /mats/1
|
||||
# PATCH/PUT /mats/1.json
|
||||
def update
|
||||
@@ -94,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
|
||||
|
||||
57
app/controllers/password_resets_controller.rb
Normal file
57
app/controllers/password_resets_controller.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
class PasswordResetsController < ApplicationController
|
||||
before_action :get_user, only: [:edit, :update]
|
||||
before_action :valid_user, only: [:edit, :update]
|
||||
before_action :check_expiration, only: [:edit, :update]
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.find_by(email: params[:password_reset][:email].downcase)
|
||||
if @user
|
||||
@user.create_reset_digest
|
||||
@user.send_password_reset_email
|
||||
redirect_to root_url, notice: "Email sent with password reset instructions"
|
||||
else
|
||||
flash.now[:alert] = "Email address not found"
|
||||
render 'new'
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if params[:user][:password].empty?
|
||||
@user.errors.add(:password, "can't be empty")
|
||||
render 'edit'
|
||||
elsif @user.update(user_params)
|
||||
session[:user_id] = @user.id
|
||||
redirect_to root_url, notice: "Password has been reset"
|
||||
else
|
||||
render 'edit'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
end
|
||||
|
||||
def get_user
|
||||
@user = User.find_by(email: params[:email])
|
||||
end
|
||||
|
||||
def valid_user
|
||||
unless @user && @user.authenticated?(:reset, params[:id])
|
||||
redirect_to root_url
|
||||
end
|
||||
end
|
||||
|
||||
def check_expiration
|
||||
if @user.password_reset_expired?
|
||||
redirect_to new_password_reset_url, alert: "Password reset has expired"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,7 @@ class SchoolsController < ApplicationController
|
||||
before_action :set_school, only: [:import_baumspage_roster, :show, :edit, :update, :destroy, :stats]
|
||||
before_action :check_access_director, only: [:new,:create,:destroy]
|
||||
before_action :check_access_delegate, only: [:import_baumspage_roster, :update,:edit]
|
||||
|
||||
before_action :check_read_access, only: [:show, :stats]
|
||||
|
||||
def stats
|
||||
@tournament = @school.tournament
|
||||
@@ -12,7 +12,7 @@ class SchoolsController < ApplicationController
|
||||
# GET /schools/1.json
|
||||
def show
|
||||
session.delete(:return_path)
|
||||
@wrestlers = @school.wrestlers.includes(:deductedPoints,:matches,:weight)
|
||||
@wrestlers = @school.wrestlers.includes(:deductedPoints, :weight, :school, :matches_as_w1, :matches_as_w2)
|
||||
@tournament = @school.tournament
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@ class SchoolsController < ApplicationController
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_school
|
||||
@school = School.where(:id => params[:id]).includes(:tournament,:wrestlers,:deductedPoints,:delegates).first
|
||||
@school = School.includes(:tournament, :delegates, :deductedPoints, wrestlers: [:weight, :deductedPoints, :matches_as_w1, :matches_as_w2]).find_by(id: params[:id])
|
||||
end
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
@@ -93,20 +93,37 @@ class SchoolsController < ApplicationController
|
||||
end
|
||||
|
||||
def check_access_director
|
||||
if params[:tournament]
|
||||
if params[:tournament].present?
|
||||
@tournament = Tournament.find(params[:tournament])
|
||||
elsif params[:school]
|
||||
elsif params[:school].present?
|
||||
@tournament = Tournament.find(params[:school]["tournament_id"])
|
||||
elsif @school
|
||||
@tournament = @school.tournament
|
||||
elsif school_params
|
||||
@tournament = Tournament.find(school_params[:tournament_id])
|
||||
end
|
||||
|
||||
authorize! :manage, @tournament
|
||||
end
|
||||
|
||||
def check_access_delegate
|
||||
if params[:school].present?
|
||||
if school_params[:school_permission_key].present?
|
||||
@school_permission_key = params[:school_permission_key]
|
||||
end
|
||||
end
|
||||
|
||||
if params[:school_permission_key].present?
|
||||
@school_permission_key = params[:school_permission_key]
|
||||
end
|
||||
|
||||
authorize! :manage, @school
|
||||
end
|
||||
|
||||
def check_read_access
|
||||
# set @school_permission_key for use in ability
|
||||
if params[:school_permission_key].present?
|
||||
@school_permission_key = params[:school_permission_key]
|
||||
end
|
||||
|
||||
authorize! :read, @school
|
||||
end
|
||||
end
|
||||
|
||||
20
app/controllers/sessions_controller.rb
Normal file
20
app/controllers/sessions_controller.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class SessionsController < ApplicationController
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
user = User.find_by(email: params[:session][:email].downcase)
|
||||
if user && user.authenticate(params[:session][:password])
|
||||
session[:user_id] = user.id
|
||||
redirect_to root_path, notice: "Logged in successfully"
|
||||
else
|
||||
flash.now[:alert] = "Invalid email/password combination"
|
||||
render 'new'
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
session.delete(:user_id)
|
||||
redirect_to root_path, notice: "Logged out successfully"
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
78
app/controllers/tournament_backups_controller.rb
Normal file
78
app/controllers/tournament_backups_controller.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
class TournamentBackupsController < ApplicationController
|
||||
before_action :set_tournament
|
||||
before_action :set_tournament_backup, only: [:show, :destroy, :restore]
|
||||
before_action :check_access_manage
|
||||
|
||||
# GET /tournament/:tournament_id/tournament_backups
|
||||
def index
|
||||
@tournament_backups = @tournament.tournament_backups.order(created_at: :desc)
|
||||
end
|
||||
|
||||
# GET /tournament/:tournament_id/tournament_backups/:id
|
||||
def show
|
||||
end
|
||||
|
||||
# DELETE /tournament/:tournament_id/tournament_backups/:id
|
||||
def destroy
|
||||
if @tournament_backup.destroy
|
||||
redirect_to tournament_tournament_backups_path(@tournament), notice: 'Backup was successfully deleted.'
|
||||
else
|
||||
redirect_to tournament_tournament_backups_path(@tournament), alert: 'Failed to delete the backup.'
|
||||
end
|
||||
end
|
||||
|
||||
# POST /tournament/:tournament_id/tournament_backups/create
|
||||
def create
|
||||
TournamentBackupService.new(@tournament, 'Manual backup').create_backup
|
||||
redirect_to tournament_tournament_backups_path(@tournament), notice: 'Backup was successfully created. It will show up soon, check your background jobs for status.'
|
||||
end
|
||||
|
||||
# POST /tournament/:tournament_id/tournament_backups/:id/restore
|
||||
def restore
|
||||
WrestlingdevImporter.new(@tournament, @tournament_backup).import
|
||||
redirect_to tournament_path(@tournament), notice: 'Restore has successfully been submitted, please check your background jobs to see if it has finished.'
|
||||
end
|
||||
|
||||
# POST /tournament/:tournament_id/tournament_backups/import_manual
|
||||
def import_manual
|
||||
import_text = params[:tournament][:import_text]
|
||||
if import_text.blank?
|
||||
redirect_to tournament_tournament_backups_path(@tournament), alert: 'Import text cannot be blank.'
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
|
||||
# Create a temporary backup object
|
||||
backup = TournamentBackup.new(
|
||||
tournament: @tournament,
|
||||
backup_data: Base64.encode64(import_text),
|
||||
backup_reason: 'Manual Import'
|
||||
)
|
||||
|
||||
# Pass the backup object to the importer
|
||||
WrestlingdevImporter.new(@tournament, backup).import
|
||||
|
||||
redirect_to tournament_path(@tournament), notice: 'Restore has successfully been submitted, please check your background jobs to see if it has finished.'
|
||||
rescue JSON::ParserError => e
|
||||
redirect_to tournament_tournament_backups_path(@tournament), alert: "Failed to parse JSON: #{e.message}"
|
||||
rescue StandardError => e
|
||||
redirect_to tournament_tournament_backups_path(@tournament), alert: "An error occurred: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tournament
|
||||
@tournament = Tournament.find(params[:tournament_id])
|
||||
end
|
||||
|
||||
def set_tournament_backup
|
||||
@tournament_backup = @tournament.tournament_backups.find(params[:id])
|
||||
end
|
||||
|
||||
def check_access_manage
|
||||
authorize! :manage, @tournament
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
class TournamentsController < ApplicationController
|
||||
before_action :set_tournament, only: [:calculate_team_scores, :import,:export,: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,:brackets,:generate_matches,:bracket,:all_brackets]
|
||||
before_action :check_access_manage, only: [:calculate_team_scores, :import,:export,: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: [: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
|
||||
|
||||
end
|
||||
|
||||
def export
|
||||
|
||||
@schools = @tournament.schools.includes(wrestlers: :weight)
|
||||
end
|
||||
|
||||
def calculate_team_scores
|
||||
@tournament.schools.each do |school|
|
||||
school.calculate_score
|
||||
end
|
||||
respond_to do |format|
|
||||
format.html { redirect_to "/tournaments/#{@tournament.id}", notice: 'Team scores are calcuating.' }
|
||||
end
|
||||
end
|
||||
|
||||
def import
|
||||
import_text = params[:tournament][:import_text]
|
||||
respond_to do |format|
|
||||
if WrestlingdevImporter.new(@tournament,import_text).import
|
||||
format.html { redirect_to "/tournaments/#{@tournament.id}", notice: 'Import is on-going. This will take 1-5 minutes.' }
|
||||
format.json { render action: 'show', status: :created, location: @tournament }
|
||||
if @tournament.calculate_all_team_scores
|
||||
format.html { redirect_to "/tournaments/#{@tournament.id}", notice: 'Team scores are calcuating.' }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -92,7 +79,7 @@ class TournamentsController < ApplicationController
|
||||
|
||||
def school_delegate
|
||||
if params[:search]
|
||||
@users = User.limit(200).search(params[:search])
|
||||
@user = User.where('email = ?', params[:search]).first
|
||||
elsif params[:school_delegate]
|
||||
@delegate = SchoolDelegate.new
|
||||
@delegate.user_id = params[:school_delegate]["user_id"]
|
||||
@@ -104,19 +91,15 @@ class TournamentsController < ApplicationController
|
||||
format.html { redirect_to "/tournaments/#{@tournament.id}/school_delegate", notice: 'There was an issue delegating permissions please try again' }
|
||||
end
|
||||
end
|
||||
else
|
||||
@users_delegates = []
|
||||
@tournament.schools.each do |s|
|
||||
s.delegates.each do |d|
|
||||
@users_delegates << d
|
||||
end
|
||||
end
|
||||
end
|
||||
@users_delegates = SchoolDelegate.includes(:user, :school)
|
||||
.joins(:school)
|
||||
.where(schools: { tournament_id: @tournament.id })
|
||||
end
|
||||
|
||||
def delegate
|
||||
if params[:search]
|
||||
@users = User.limit(200).search(params[:search])
|
||||
@user = User.where('email = ?', params[:search]).first
|
||||
elsif params[:tournament_delegate]
|
||||
@delegate = TournamentDelegate.new
|
||||
@delegate.user_id = params[:tournament_delegate]["user_id"]
|
||||
@@ -128,13 +111,64 @@ class TournamentsController < ApplicationController
|
||||
format.html { redirect_to "/tournaments/#{@tournament.id}/delegate", notice: 'There was an issue delegating permissions please try again' }
|
||||
end
|
||||
end
|
||||
else
|
||||
@users_delegates = @tournament.delegates
|
||||
end
|
||||
@users_delegates = @tournament.delegates.includes(:user)
|
||||
end
|
||||
|
||||
def matches
|
||||
@matches = @tournament.matches.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
|
||||
@@ -144,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
|
||||
@@ -165,7 +207,7 @@ class TournamentsController < ApplicationController
|
||||
end
|
||||
|
||||
def create_custom_weights
|
||||
@custom = params[:customValue].to_s
|
||||
@custom = params[:customValue].split(",")
|
||||
@tournament.create_pre_defined_weights(@custom)
|
||||
redirect_to "/tournaments/#{@tournament.id}"
|
||||
end
|
||||
@@ -174,18 +216,30 @@ class TournamentsController < ApplicationController
|
||||
def all_brackets
|
||||
@schools = @tournament.schools
|
||||
@schools = @schools.sort_by{|s| s.page_score_string}.reverse!
|
||||
@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
|
||||
if params[:weight]
|
||||
@weight = Weight.where(:id => params[:weight]).includes(:matches,:wrestlers).first
|
||||
@matches = @weight.matches
|
||||
@wrestlers = @weight.wrestlers.includes(:school)
|
||||
if @tournament.tournament_type == "Pool to bracket"
|
||||
@pools = @weight.pool_rounds(@matches)
|
||||
@bracketType = @weight.pool_bracket_type
|
||||
end
|
||||
if params[:weight]
|
||||
@weight = Weight.includes(:matches, wrestlers: [:school, :matches_as_w1, :matches_as_w2]).find_by(id: params[:weight])
|
||||
@matches = @weight.matches
|
||||
@wrestlers = @weight.wrestlers
|
||||
|
||||
if @tournament.tournament_type == "Pool to bracket"
|
||||
@pools = @weight.pool_rounds(@matches)
|
||||
@bracketType = @weight.pool_bracket_type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def all_results
|
||||
@matches = @tournament.matches.includes(:schools,:wrestlers,:weight)
|
||||
@round = nil
|
||||
@bracket_position = nil
|
||||
end
|
||||
|
||||
def generate_matches
|
||||
@@ -202,35 +256,68 @@ class TournamentsController < ApplicationController
|
||||
|
||||
end
|
||||
|
||||
def qrcode
|
||||
@tournament_url = tournament_url(@tournament)
|
||||
@qrcode = RQRCode::QRCode.new(@tournament_url)
|
||||
end
|
||||
|
||||
|
||||
def up_matches
|
||||
@matches = @tournament.matches.where("mat_id is NULL and (finished <> ? or finished is NULL)",1).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
|
||||
|
||||
def index
|
||||
if params[:search]
|
||||
@tournaments = Tournament.limit(200).search(params[:search]).order("created_at DESC")
|
||||
# Simple manual pagination to avoid introducing a gem.
|
||||
per_page = 20
|
||||
page = params[:page].to_i > 0 ? params[:page].to_i : 1
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
if params[:search].present?
|
||||
tournaments = Tournament.search_date_name(params[:search]).to_a
|
||||
else
|
||||
@tournaments = Tournament.all.sort_by{|t| t.days_until_start}.first(20)
|
||||
tournaments = Tournament.all.to_a
|
||||
end
|
||||
|
||||
# Sort by distance from today (closest first)
|
||||
today = Date.today
|
||||
tournaments.sort_by! { |t| (t.date - today).abs }
|
||||
|
||||
@total_count = tournaments.size
|
||||
@total_pages = (@total_count / per_page.to_f).ceil
|
||||
@page = page
|
||||
@per_page = per_page
|
||||
@tournaments = tournaments.slice(offset, per_page) || []
|
||||
end
|
||||
|
||||
def show
|
||||
@schools = @tournament.schools.includes(:delegates)
|
||||
@tournament = Tournament.find(params[:id])
|
||||
@schools = @tournament.schools.includes(:delegates).sort_by{|school|school.name}
|
||||
@weights = @tournament.weights.sort_by{|x|[x.max]}
|
||||
@mats = @tournament.mats
|
||||
@mats = @tournament.mats.sort_by{|mat|mat.name}
|
||||
end
|
||||
|
||||
def new
|
||||
@@ -246,6 +333,7 @@ class TournamentsController < ApplicationController
|
||||
redirect_to root_path
|
||||
end
|
||||
@tournament = Tournament.new(tournament_params)
|
||||
@tournament.user_id = current_user.id
|
||||
respond_to do |format|
|
||||
if @tournament.save
|
||||
format.html { redirect_to @tournament, notice: 'Tournament was successfully created.' }
|
||||
@@ -280,15 +368,32 @@ class TournamentsController < ApplicationController
|
||||
def error
|
||||
end
|
||||
|
||||
def reset_bout_board
|
||||
@tournament.reset_and_fill_bout_board
|
||||
redirect_to tournament_path(@tournament), notice: "Successfully reset the bout board. Please have all mat table workers refresh their page."
|
||||
end
|
||||
|
||||
def generate_school_keys
|
||||
@tournament.schools.each do |school|
|
||||
school.update(permission_key: SecureRandom.uuid)
|
||||
end
|
||||
redirect_to school_delegate_path(@tournament), notice: "School permission keys generated successfully."
|
||||
end
|
||||
|
||||
def delete_school_keys
|
||||
@tournament.schools.update_all(permission_key: nil)
|
||||
redirect_to school_delegate_path(@tournament), notice: "All school permission keys have been deleted."
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_tournament
|
||||
@tournament = Tournament.where(:id => params[:id]).includes(:schools,:weights,:mats,:matches,:user,:wrestlers).first
|
||||
@tournament = Tournament.includes(:user, :mats, :schools, :weights, :matches, wrestlers: [:school, :weight, :matches_as_w1, :matches_as_w2]).find_by(id: params[:id])
|
||||
end
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def tournament_params
|
||||
params.require(:tournament).permit(:name, :address, :director, :director_email, :tournament_type, :weigh_in_ref, :user_id, :date, :originalId, :swapId)
|
||||
params.require(:tournament).permit(:name, :address, :director, :director_email, :tournament_type, :weigh_in_ref, :date, :originalId, :swapId, :is_public)
|
||||
end
|
||||
|
||||
#Check for tournament owner
|
||||
@@ -300,6 +405,10 @@ class TournamentsController < ApplicationController
|
||||
authorize! :manage, @tournament
|
||||
end
|
||||
|
||||
def check_access_read
|
||||
authorize! :read, @tournament
|
||||
end
|
||||
|
||||
def check_for_matches
|
||||
if @tournament
|
||||
if @tournament.matches.empty? or @tournament.curently_generating_matches == 1
|
||||
|
||||
48
app/controllers/users_controller.rb
Normal file
48
app/controllers/users_controller.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
class UsersController < ApplicationController
|
||||
before_action :require_login, only: [:edit, :update]
|
||||
before_action :correct_user, only: [:edit, :update]
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.new(user_params)
|
||||
if @user.save
|
||||
session[:user_id] = @user.id
|
||||
redirect_to root_path, notice: "Account created successfully"
|
||||
else
|
||||
render 'new'
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
def update
|
||||
@user = User.find(params[:id])
|
||||
if @user.update(user_params)
|
||||
redirect_to root_path, notice: "Account updated successfully"
|
||||
else
|
||||
render 'edit'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :password, :password_confirmation)
|
||||
end
|
||||
|
||||
def require_login
|
||||
unless current_user
|
||||
redirect_to login_path, alert: "Please log in to access this page"
|
||||
end
|
||||
end
|
||||
|
||||
def correct_user
|
||||
@user = User.find(params[:id])
|
||||
redirect_to root_path unless current_user == @user
|
||||
end
|
||||
end
|
||||
@@ -1,21 +1,28 @@
|
||||
class WeightsController < ApplicationController
|
||||
before_action :set_weight, only: [:pool_order, :show, :edit, :update, :destroy,:re_gen]
|
||||
before_action :check_access, only: [:pool_order, :new,:create,:update,:destroy,:edit, :re_gen]
|
||||
before_action :set_weight, only: [:pool_order, :show, :edit, :update, :destroy]
|
||||
before_action :check_access_manage, only: [:pool_order, :new,:create,:update,:destroy,:edit]
|
||||
before_action :check_access_read, only: [:show]
|
||||
|
||||
|
||||
# GET /weights/1
|
||||
# GET /weights/1.json
|
||||
def show
|
||||
if params[:wrestler]
|
||||
check_access_manage
|
||||
respond_to do |format|
|
||||
Wrestler.update(params[:wrestler].keys, params[:wrestler].values)
|
||||
# Sanitize the wrestler parameters
|
||||
sanitized_wrestlers = params.require(:wrestler).to_unsafe_h.transform_values do |attributes|
|
||||
ActionController::Parameters.new(attributes).permit(:original_seed)
|
||||
end
|
||||
|
||||
Wrestler.update(sanitized_wrestlers.keys, sanitized_wrestlers.values)
|
||||
format.html { redirect_to @weight, notice: 'Seeds were successfully updated.' }
|
||||
end
|
||||
end
|
||||
@wrestlers = @weight.wrestlers
|
||||
@tournament = @weight.tournament
|
||||
session[:return_path] = "/weights/#{@weight.id}"
|
||||
end
|
||||
end
|
||||
|
||||
# GET /weights/new
|
||||
def new
|
||||
@@ -72,11 +79,6 @@ class WeightsController < ApplicationController
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def re_gen
|
||||
@tournament = @weight.tournament
|
||||
GenerateTournamentMatches.new(@tournament).generateWeight(@weight)
|
||||
end
|
||||
|
||||
def pool_order
|
||||
pool = params[:pool_to_order].to_i
|
||||
@@ -96,14 +98,15 @@ class WeightsController < ApplicationController
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_weight
|
||||
@weight = Weight.where(:id => params[:id]).includes(:tournament,:wrestlers).first
|
||||
# Add nested includes for wrestlers
|
||||
@weight = Weight.includes(:tournament, wrestlers: [:school, :matches_as_w1, :matches_as_w2]).find_by(id: params[:id])
|
||||
end
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def weight_params
|
||||
params.require(:weight).permit(:max, :tournament_id, :mat_id)
|
||||
end
|
||||
def check_access
|
||||
def check_access_manage
|
||||
if params[:tournament]
|
||||
@tournament = Tournament.find(params[:tournament])
|
||||
elsif params[:weight]
|
||||
@@ -114,5 +117,16 @@ class WeightsController < ApplicationController
|
||||
authorize! :manage, @tournament
|
||||
end
|
||||
|
||||
def check_access_read
|
||||
if params[:tournament]
|
||||
@tournament = Tournament.find(params[:tournament])
|
||||
elsif params[:weight]
|
||||
@tournament = Tournament.find(params[:weight]["tournament_id"])
|
||||
elsif @weight
|
||||
@tournament = @weight.tournament
|
||||
end
|
||||
authorize! :read, @tournament
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
class WrestlersController < ApplicationController
|
||||
before_action :set_wrestler, only: [:show, :edit, :update, :destroy, :update_pool]
|
||||
before_action :check_access, only: [:new,:create,:update,:destroy,:edit,:update_pool]
|
||||
|
||||
|
||||
|
||||
before_action :set_wrestler, only: [:show, :edit, :update, :destroy]
|
||||
before_action :check_access, only: [:new, :create, :update, :destroy, :edit]
|
||||
before_action :check_read_access, only: [:show]
|
||||
|
||||
# GET /wrestlers/1
|
||||
# GET /wrestlers/1.json
|
||||
@@ -16,132 +14,149 @@ class WrestlersController < ApplicationController
|
||||
# GET /wrestlers/new
|
||||
def new
|
||||
@wrestler = Wrestler.new
|
||||
if params[:school]
|
||||
@school = School.find(params[:school])
|
||||
end
|
||||
if @school
|
||||
@tournament = Tournament.find(@school.tournament_id)
|
||||
end
|
||||
if @tournament
|
||||
@weights = Weight.where(tournament_id: @tournament.id).sort_by{|w| w.max}
|
||||
end
|
||||
|
||||
@school = School.find_by(id: params[:school]) if params[:school]
|
||||
# Save the key into an instance variable so the view can use it.
|
||||
@school_permission_key = params[:school_permission_key].presence
|
||||
@tournament = @school.tournament if @school
|
||||
@weights = @tournament.weights.sort_by(&:max) if @tournament
|
||||
end
|
||||
|
||||
# GET /wrestlers/1/edit
|
||||
def edit
|
||||
@tournament = @wrestler.tournament
|
||||
@weight = @wrestler.weight
|
||||
@weights = @school.tournament.weights.sort_by{|w| w.max}
|
||||
@school = @wrestler.school
|
||||
@weights = @school.tournament.weights.sort_by(&:max)
|
||||
end
|
||||
|
||||
# POST /wrestlers
|
||||
# POST /wrestlers.json
|
||||
def create
|
||||
@wrestler = Wrestler.new(wrestler_params)
|
||||
@school = School.find(wrestler_params[:school_id])
|
||||
@weights = @school.tournament.weights
|
||||
@school = School.find_by(id: wrestler_params[:school_id])
|
||||
# IMPORTANT: Get the key from wrestler_params (not from params directly)
|
||||
@school_permission_key = wrestler_params[:school_permission_key].presence
|
||||
@weights = @school.tournament.weights if @school
|
||||
|
||||
# Remove the key from attributes so it isn't assigned to the model.
|
||||
@wrestler = Wrestler.new(wrestler_params.except(:school_permission_key))
|
||||
|
||||
respond_to do |format|
|
||||
if @wrestler.save
|
||||
if session[:return_path]
|
||||
format.html { redirect_to session.delete(:return_path), notice: 'Wrestler was successfully created.' }
|
||||
else
|
||||
format.html { redirect_to @school, notice: 'Wrestler was successfully created.' }
|
||||
format.json { render action: 'show', status: :created, location: @wrestler }
|
||||
end
|
||||
redirect_path = session[:return_path] || school_path(@school)
|
||||
format.html { redirect_to append_permission_key(redirect_path), notice: 'Wrestler was successfully created.' }
|
||||
format.json { render :show, status: :created, location: @wrestler }
|
||||
else
|
||||
format.html { render action: 'new' }
|
||||
format.html { render :new }
|
||||
format.json { render json: @wrestler.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /wrestlers/1
|
||||
# PATCH/PUT /wrestlers/1.json
|
||||
def update
|
||||
@tournament = @wrestler.tournament
|
||||
@weight = @wrestler.weight
|
||||
@weights = @tournament.weights.sort_by{|w| w.max}
|
||||
@school = @wrestler.school
|
||||
@weights = @tournament.weights.sort_by(&:max)
|
||||
|
||||
respond_to do |format|
|
||||
if @wrestler.update(wrestler_params)
|
||||
if session[:return_path]
|
||||
format.html { redirect_to session.delete(:return_path), notice: 'Wrestler was successfully updated.' }
|
||||
else
|
||||
format.html { redirect_to @school, notice: 'Wrestler was successfully updated.' }
|
||||
format.json { render action: 'show', status: :created, location: @wrestler }
|
||||
end
|
||||
if @wrestler.update(wrestler_params.except(:school_permission_key))
|
||||
redirect_path = session[:return_path] || school_path(@school)
|
||||
format.html { redirect_to append_permission_key(redirect_path), notice: 'Wrestler was successfully updated.' }
|
||||
format.json { render :show, status: :ok, location: @wrestler }
|
||||
else
|
||||
format.html { render action: 'edit' }
|
||||
format.html { render :edit }
|
||||
format.json { render json: @wrestler.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_pool
|
||||
@tournament = @wrestler.tournament
|
||||
@weight = @wrestler.weight
|
||||
@weights = @tournament.weights.sort_by{|w| w.max}
|
||||
@school = @wrestler.school
|
||||
if params[:wrestler]['pool']
|
||||
@wrestler.pool = params[:wrestler]['pool']
|
||||
respond_to do |format|
|
||||
message = "Wrestler has successfully been switched to a new pool. Matches for that weight are now in a weird state. Please re-generate matches when you are done with all of your changes."
|
||||
if @wrestler.update(wrestler_params)
|
||||
format.html { redirect_to "/tournaments/#{@tournament.id}/brackets/#{@wrestler.weight.id}/", notice: message }
|
||||
format.json { head :no_content }
|
||||
else
|
||||
format.html { render action: 'edit' }
|
||||
format.json { render json: @wrestler.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /wrestlers/1
|
||||
# DELETE /wrestlers/1.json
|
||||
def destroy
|
||||
@school = @wrestler.school
|
||||
@wrestler.destroy
|
||||
message = "Wrestler was successfully deleted. This action has removed all matches. Please re-generate matches if you already had matches."
|
||||
|
||||
respond_to do |format|
|
||||
message = "Wrestler was successfully deleted. This action has removed all matches. Please re-generate matches if you already had matches."
|
||||
if session[:return_path]
|
||||
format.html { redirect_to session.delete(:return_path), notice: message }
|
||||
else
|
||||
format.html { redirect_to @school, notice: message }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
redirect_path = session[:return_path] || school_path(@school)
|
||||
format.html { redirect_to append_permission_key(redirect_path), notice: message }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_wrestler
|
||||
@wrestler = Wrestler.where(:id => params[:id]).includes(:school, :weight, :tournament, :matches).first
|
||||
|
||||
def set_wrestler
|
||||
@wrestler = Wrestler.includes(:school, :weight, :tournament, :matches_as_w1, :matches_as_w2).find_by(id: params[:id])
|
||||
|
||||
if @wrestler.nil?
|
||||
redirect_to root_path, alert: "Wrestler not found"
|
||||
end
|
||||
end
|
||||
|
||||
def wrestler_params
|
||||
params.require(:wrestler).permit(:name, :school_id, :weight_id, :seed, :original_seed, :season_win,
|
||||
:season_loss, :criteria, :extra, :offical_weight, :pool, :school_permission_key)
|
||||
end
|
||||
|
||||
def check_access
|
||||
if params[:school].present?
|
||||
@school = School.find(params[:school])
|
||||
#@tournament = Tournament.find(@school.tournament.id)
|
||||
elsif params[:wrestler].present?
|
||||
if params[:wrestler]["school_id"].present?
|
||||
@school = School.find(params[:wrestler]["school_id"])
|
||||
if wrestler_params[:school_permission_key].present?
|
||||
@school_permission_key = wrestler_params[:school_permission_key]
|
||||
end
|
||||
else
|
||||
@wrestler = Wrestler.find(params[:wrestler]["id"])
|
||||
@school = @wrestler.school
|
||||
end
|
||||
elsif @wrestler
|
||||
@school = @wrestler.school
|
||||
end
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def wrestler_params
|
||||
params.require(:wrestler).permit(:name, :school_id, :weight_id, :seed, :original_seed, :season_win, :season_loss,:criteria,:extra,:offical_weight,:pool)
|
||||
# set @school_permission_key for use in ability
|
||||
if params[:school_permission_key].present?
|
||||
@school_permission_key = params[:school_permission_key]
|
||||
end
|
||||
def check_access
|
||||
if params[:school]
|
||||
@school = School.find(params[:school])
|
||||
#@tournament = Tournament.find(@school.tournament.id)
|
||||
elsif params[:wrestler]
|
||||
if params[:wrestler]["school_id"]
|
||||
@school = School.find(params[:wrestler]["school_id"])
|
||||
else
|
||||
@wrestler = Wrestler.find(params[:wrestler]["id"])
|
||||
@school = @wrestler.school
|
||||
end
|
||||
#@tournament = Tournament.find(@school.tournament.id)
|
||||
elsif @wrestler
|
||||
@school = @wrestler.school
|
||||
#@tournament = @wrestler.tournament
|
||||
elsif wrestler_params
|
||||
@school = School.find(wrestler_params[:school_id])
|
||||
end
|
||||
authorize! :manage, @school
|
||||
authorize! :manage, @school
|
||||
end
|
||||
|
||||
def check_read_access
|
||||
if params[:school]
|
||||
@school = School.find(params[:school])
|
||||
elsif params[:wrestler].present?
|
||||
if params[:wrestler]["school_id"].present?
|
||||
@school = School.find(params[:wrestler]["school_id"])
|
||||
else
|
||||
@wrestler = Wrestler.find(params[:wrestler]["id"])
|
||||
@school = @wrestler.school
|
||||
end
|
||||
if wrestler_params[:school_permission_key].present?
|
||||
@school_permission_key = wrestler_params[:school_permission_key]
|
||||
end
|
||||
elsif @wrestler
|
||||
@school = @wrestler.school
|
||||
end
|
||||
|
||||
# set @school_permission_key for use in ability
|
||||
if params[:school_permission_key].present?
|
||||
@school_permission_key = params[:school_permission_key]
|
||||
end
|
||||
authorize! :read, @school
|
||||
end
|
||||
|
||||
# Helper method to append school_permission_key to redirects if it exists.
|
||||
def append_permission_key(path)
|
||||
return path unless @school_permission_key.present?
|
||||
|
||||
# If path is an ActiveRecord object, convert to URL.
|
||||
path = school_path(path) if path.is_a?(School)
|
||||
uri = URI.parse(path)
|
||||
query_params = Rack::Utils.parse_nested_query(uri.query || "")
|
||||
query_params["school_permission_key"] = @school_permission_key
|
||||
uri.query = query_params.to_query
|
||||
uri.to_s
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
34
app/jobs/advance_wrestler_job.rb
Normal file
34
app/jobs/advance_wrestler_job.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
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, tournament_id)
|
||||
# Get tournament from wrestler
|
||||
tournament = wrestler.tournament
|
||||
|
||||
# Create job status record
|
||||
job_name = "Advancing wrestler #{wrestler.name}"
|
||||
job_status = TournamentJobStatus.create!(
|
||||
tournament: tournament,
|
||||
job_name: job_name,
|
||||
status: "Running",
|
||||
details: "Match ID: #{match&.bout_number || 'No match'} Wrestler Name #{wrestler&.name || 'No Wrestler'}"
|
||||
)
|
||||
|
||||
begin
|
||||
# Execute the job
|
||||
service = AdvanceWrestler.new(wrestler, match)
|
||||
service.advance_raw
|
||||
|
||||
# Remove the job status record on success
|
||||
TournamentJobStatus.complete_job(tournament.id, job_name)
|
||||
rescue => e
|
||||
# Update status to errored
|
||||
job_status.update(status: "Errored", details: "Error: #{e.message}")
|
||||
|
||||
# Re-raise the error for SolidQueue to handle
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
7
app/jobs/application_job.rb
Normal file
7
app/jobs/application_job.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class ApplicationJob < ActiveJob::Base
|
||||
# Automatically retry jobs that encountered a deadlock
|
||||
# retry_on ActiveRecord::Deadlocked
|
||||
|
||||
# Most jobs are safe to ignore if the underlying records are no longer available
|
||||
# discard_on ActiveJob::DeserializationError
|
||||
end
|
||||
39
app/jobs/calculate_school_score_job.rb
Normal file
39
app/jobs/calculate_school_score_job.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
class CalculateSchoolScoreJob < ApplicationJob
|
||||
queue_as :default
|
||||
limits_concurrency to: 1, key: ->(school) { "tournament:#{school.tournament_id}" }
|
||||
|
||||
# Need for TournamentJobStatusIntegrationTest
|
||||
def self.perform_sync(school)
|
||||
# Execute directly on provided objects
|
||||
school.calculate_score_raw
|
||||
end
|
||||
|
||||
def perform(school)
|
||||
# Log information about the job
|
||||
Rails.logger.info("Calculating score for school ##{school.id} (#{school.name})")
|
||||
|
||||
# Create job status record
|
||||
tournament = school.tournament
|
||||
job_name = "Calculating team score for #{school.name}"
|
||||
job_status = TournamentJobStatus.create!(
|
||||
tournament: tournament,
|
||||
job_name: job_name,
|
||||
status: "Running",
|
||||
details: "School ID: #{school.id}"
|
||||
)
|
||||
|
||||
begin
|
||||
# Execute the calculation
|
||||
school.calculate_score_raw
|
||||
|
||||
# Remove the job status record on success
|
||||
TournamentJobStatus.complete_job(tournament.id, job_name)
|
||||
rescue => e
|
||||
# Update status to errored
|
||||
job_status.update(status: "Errored", details: "Error: #{e.message}")
|
||||
|
||||
# Re-raise the error for SolidQueue to handle
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
21
app/jobs/generate_tournament_matches_job.rb
Normal file
21
app/jobs/generate_tournament_matches_job.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class GenerateTournamentMatchesJob < ApplicationJob
|
||||
queue_as :default
|
||||
limits_concurrency to: 1, key: ->(tournament) { "tournament:#{tournament.id}" }
|
||||
|
||||
def perform(tournament)
|
||||
# Log information about the job
|
||||
Rails.logger.info("Starting tournament match generation for tournament ##{tournament.id}")
|
||||
|
||||
begin
|
||||
# Execute the job
|
||||
generator = GenerateTournamentMatches.new(tournament)
|
||||
generator.generate_raw
|
||||
|
||||
Rails.logger.info("Completed tournament match generation for tournament ##{tournament.id}")
|
||||
rescue => e
|
||||
Rails.logger.error("Error generating tournament matches: #{e.message}")
|
||||
Rails.logger.error(e.backtrace.join("\n"))
|
||||
raise # Re-raise the error so it's properly recorded
|
||||
end
|
||||
end
|
||||
end
|
||||
33
app/jobs/tournament_backup_job.rb
Normal file
33
app/jobs/tournament_backup_job.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
Rails.logger.info("Creating backup for tournament ##{tournament.id} (#{tournament.name}), reason: #{reason || 'manual'}")
|
||||
|
||||
# Create job status record
|
||||
job_name = "Backing up tournament"
|
||||
job_status = TournamentJobStatus.create!(
|
||||
tournament: tournament,
|
||||
job_name: job_name,
|
||||
status: "Running",
|
||||
details: "Reason: #{reason || 'manual'}"
|
||||
)
|
||||
|
||||
begin
|
||||
# Execute the backup
|
||||
service = TournamentBackupService.new(tournament, reason)
|
||||
service.create_backup_raw
|
||||
|
||||
# Remove the job status record on success
|
||||
TournamentJobStatus.complete_job(tournament.id, job_name)
|
||||
rescue => e
|
||||
# Update status to errored
|
||||
job_status.update(status: "Errored", details: "Error: #{e.message}")
|
||||
|
||||
# Re-raise the error for SolidQueue to handle
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/jobs/tournament_cleanup_job.rb
Normal file
37
app/jobs/tournament_cleanup_job.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
class TournamentCleanupJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform
|
||||
# Remove or clean up tournaments based on age and match status
|
||||
process_old_tournaments
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_old_tournaments
|
||||
# Get all tournaments older than 1 week that have a user_id
|
||||
old_tournaments = Tournament.where('date < ? AND user_id IS NOT NULL', 1.week.ago.to_date)
|
||||
|
||||
old_tournaments.each do |tournament|
|
||||
# Check if it has any non-BYE finished matches
|
||||
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
|
||||
end
|
||||
|
||||
# 2. Remove all tournament delegates
|
||||
tournament.delegates.destroy_all
|
||||
|
||||
# 3. Set user_id to null
|
||||
tournament.update(user_id: nil)
|
||||
else
|
||||
tournament.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
app/jobs/wrestlingdev_import_job.rb
Normal file
34
app/jobs/wrestlingdev_import_job.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
Rails.logger.info("Starting import for tournament ##{tournament.id} (#{tournament.name})")
|
||||
|
||||
# Create job status record
|
||||
job_name = "Importing tournament"
|
||||
job_status = TournamentJobStatus.create!(
|
||||
tournament: tournament,
|
||||
job_name: job_name,
|
||||
status: "Running",
|
||||
details: "Processing backup data"
|
||||
)
|
||||
|
||||
begin
|
||||
# Execute the import
|
||||
importer = WrestlingdevImporter.new(tournament)
|
||||
importer.import_data = import_data if import_data
|
||||
importer.import_raw
|
||||
|
||||
# Remove the job status record on success
|
||||
TournamentJobStatus.complete_job(tournament.id, job_name)
|
||||
rescue => e
|
||||
# Update status to errored
|
||||
job_status.update(status: "Errored", details: "Error: #{e.message}")
|
||||
|
||||
# Re-raise the error for SolidQueue to handle
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
4
app/mailers/application_mailer.rb
Normal file
4
app/mailers/application_mailer.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: ENV["WRESTLINGDEV_EMAIL"] || 'noreply@wrestlingdev.com'
|
||||
layout 'mailer'
|
||||
end
|
||||
11
app/mailers/user_mailer.rb
Normal file
11
app/mailers/user_mailer.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class UserMailer < ApplicationMailer
|
||||
# Subject can be set in your I18n file at config/locales/en.yml
|
||||
# with the following lookup:
|
||||
#
|
||||
# en.user_mailer.password_reset.subject
|
||||
#
|
||||
def password_reset(user)
|
||||
@user = user
|
||||
mail to: user.email, subject: "WrestlingDev - Password reset"
|
||||
end
|
||||
end
|
||||
@@ -1,58 +1,80 @@
|
||||
class Ability
|
||||
include CanCan::Ability
|
||||
|
||||
def initialize(user)
|
||||
# Define abilities for the passed in user here. For example:
|
||||
#
|
||||
# user ||= User.new # guest user (not logged in)
|
||||
# if user.admin?
|
||||
# can :manage, :all
|
||||
# else
|
||||
# can :read, :all
|
||||
# end
|
||||
#
|
||||
# The first argument to `can` is the action you are giving the user
|
||||
# permission to do.
|
||||
# If you pass :manage it will apply to every action. Other common actions
|
||||
# here are :read, :create, :update and :destroy.
|
||||
#
|
||||
# The second argument is the resource the user can perform the action on.
|
||||
# If you pass :all it will apply to every resource. Otherwise pass a Ruby
|
||||
# class of the resource.
|
||||
#
|
||||
# The third argument is an optional hash of conditions to further filter the
|
||||
# objects.
|
||||
# For example, here the user can only update published articles.
|
||||
#
|
||||
# can :update, Article, :published => true
|
||||
#
|
||||
# See the wiki for details:
|
||||
# https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities
|
||||
if !user.nil?
|
||||
#Can manage tournament if tournament owner
|
||||
can :manage, Tournament, :user_id => user.id
|
||||
#Can manage but cannot destroy tournament if tournament delegate
|
||||
def school_permission_key_check(school_permission_key)
|
||||
# Can read school if tournament is public or a valid school permission key is provided
|
||||
can :read, School do |school|
|
||||
school.tournament.is_public ||
|
||||
(school_permission_key.present? && school.permission_key == school_permission_key)
|
||||
end
|
||||
|
||||
# Can manage school if a valid school permission key is provided
|
||||
# school_permission_key comes from app/controllers/application_controller.rb
|
||||
can :manage, School do |school|
|
||||
(school_permission_key.present? && school.permission_key == school_permission_key)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(user, school_permission_key = nil)
|
||||
if user
|
||||
# LOGGED IN USER PERMISSIONS
|
||||
|
||||
# TOURNAMENT PERMISSIONS
|
||||
|
||||
# Can manage but cannot destroy tournament if tournament delegate
|
||||
can :manage, Tournament do |tournament|
|
||||
tournament.delegates.map(&:user_id).include? user.id
|
||||
tournament.user_id == user.id ||
|
||||
tournament.delegates.map(&:user_id).include?(user.id)
|
||||
end
|
||||
|
||||
# can destroy tournament if tournament owner
|
||||
can :destroy, Tournament do |tournament|
|
||||
tournament.user_id == user.id
|
||||
end
|
||||
# tournament delegates cannot destroy - explicitly deny
|
||||
cannot :destroy, Tournament do |tournament|
|
||||
tournament.delegates.map(&:user_id).include? user.id
|
||||
tournament.delegates.map(&:user_id).include?(user.id)
|
||||
end
|
||||
#Can manage school if tournament owner
|
||||
|
||||
# Can read tournament if tournament.is_public, tournament owner, or tournament delegate
|
||||
can :read, Tournament do |tournament|
|
||||
tournament.is_public ||
|
||||
tournament.delegates.map(&:user_id).include?(user.id) ||
|
||||
tournament.user_id == user.id
|
||||
end
|
||||
|
||||
# SCHOOL PERMISSIONS
|
||||
# wrestler permissions are included with school permissions
|
||||
|
||||
# Can manage school if is school delegate, is tournament delegate, or is tournament director
|
||||
can :manage, School do |school|
|
||||
school.delegates.map(&:user_id).include?(user.id) ||
|
||||
school.tournament.delegates.map(&:user_id).include?(user.id) ||
|
||||
school.tournament.user_id == user.id
|
||||
end
|
||||
#Can manage school if tournament delegate
|
||||
can :manage, School do |school|
|
||||
school.tournament.delegates.map(&:user_id).include? user.id
|
||||
|
||||
# Can read school if tournament.is_public OR is school delegate, is tournament delegate, or is tournament director
|
||||
can :read, School do |school|
|
||||
school.tournament.is_public ||
|
||||
school.delegates.map(&:user_id).include?(user.id) ||
|
||||
school.tournament.delegates.map(&:user_id).include?(user.id) ||
|
||||
school.tournament.user_id == user.id
|
||||
end
|
||||
#Can manage but cannot destroy school if school delegate
|
||||
can :manage, School do |school|
|
||||
school.delegates.map(&:user_id).include? user.id
|
||||
end
|
||||
cannot :destroy, School do |school|
|
||||
school.delegates.map(&:user_id).include? user.id
|
||||
|
||||
school_permission_key_check(school_permission_key)
|
||||
else
|
||||
# NON LOGGED IN USER PERMISSIONS
|
||||
|
||||
# TOURNAMENT PERMISSIONS
|
||||
|
||||
# Can read tournament if tournament is public
|
||||
can :read, Tournament do |tournament|
|
||||
tournament.is_public
|
||||
end
|
||||
|
||||
# SCHOOL PERMISSIONS
|
||||
# wrestler permissions are included with school permissions
|
||||
school_permission_key_check(school_permission_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
3
app/models/application_record.rb
Normal file
3
app/models/application_record.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
||||
@@ -1,36 +1,289 @@
|
||||
class Mat < ActiveRecord::Base
|
||||
class Mat < ApplicationRecord
|
||||
include ActionView::RecordIdentifier
|
||||
belongs_to :tournament
|
||||
has_many :matches
|
||||
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
|
||||
t_matches = tournament.matches.select{|m| m.mat_id == nil && m.finished != 1 && m.bout_number != nil}
|
||||
if t_matches.size > 0
|
||||
match = t_matches.sort_by{|m| m.bout_number}.first
|
||||
match.mat_id = self.id
|
||||
match.save
|
||||
slot = first_empty_queue_slot
|
||||
return true unless slot
|
||||
|
||||
match = next_eligible_match
|
||||
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
|
||||
else
|
||||
if queue1_match&.finished == 1
|
||||
shift_queue_forward!
|
||||
end
|
||||
fill_queue_slots!
|
||||
end
|
||||
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 = 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
|
||||
.order(:bout_number)
|
||||
|
||||
# Filter out BYE matches
|
||||
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|
|
||||
if rule.weight_classes.any?
|
||||
# Ensure weight_classes is treated as an array
|
||||
filtered_matches = filtered_matches.where(weight_id: Array(rule.weight_classes).map(&:to_i))
|
||||
end
|
||||
|
||||
if rule.bracket_positions.any?
|
||||
# Ensure bracket_positions is treated as an array
|
||||
filtered_matches = filtered_matches.where(bracket_position: Array(rule.bracket_positions).map(&:to_s))
|
||||
end
|
||||
|
||||
if rule.rounds.any?
|
||||
# Ensure rounds is treated as an array
|
||||
filtered_matches = filtered_matches.where(round: Array(rule.rounds).map(&:to_i))
|
||||
end
|
||||
end
|
||||
|
||||
# Return the first match in filtered results, or nil if none are left
|
||||
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
|
||||
|
||||
29
app/models/mat_assignment_rule.rb
Normal file
29
app/models/mat_assignment_rule.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
class MatAssignmentRule < ApplicationRecord
|
||||
belongs_to :mat
|
||||
belongs_to :tournament
|
||||
|
||||
# Convert comma-separated values to arrays
|
||||
def weight_classes
|
||||
(super || "").split(",").map(&:to_i)
|
||||
end
|
||||
|
||||
def weight_classes=(value)
|
||||
super(value.is_a?(Array) ? value.join(",") : value)
|
||||
end
|
||||
|
||||
def bracket_positions
|
||||
(super || "").split(",")
|
||||
end
|
||||
|
||||
def bracket_positions=(value)
|
||||
super(value.is_a?(Array) ? value.join(",") : value)
|
||||
end
|
||||
|
||||
def rounds
|
||||
(super || "").split(",").map(&:to_i)
|
||||
end
|
||||
|
||||
def rounds=(value)
|
||||
super(value.is_a?(Array) ? value.join(",") : value)
|
||||
end
|
||||
end
|
||||
@@ -1,31 +1,106 @@
|
||||
class Match < ActiveRecord::Base
|
||||
class Match < ApplicationRecord
|
||||
include ActionView::RecordIdentifier
|
||||
|
||||
belongs_to :tournament, touch: true
|
||||
belongs_to :weight, touch: true
|
||||
belongs_to :mat, 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
|
||||
after_update :after_finished_actions, :if => :saved_change_to_finished? or :saved_change_to_winner_id? or :saved_change_to_win_type? or :saved_change_to_score?
|
||||
has_many :schools, :through => :wrestlers
|
||||
validate :score_validation, :win_type_validation, :bracket_position_validation, :overtime_type_validation
|
||||
|
||||
# Callback to update finished_at when a match is finished
|
||||
before_save :update_finished_at
|
||||
|
||||
# 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
|
||||
# will see the committed state of the match (e.g. finished == 1). Enqueuing
|
||||
# jobs from after_update can cause jobs to run before the transaction commits,
|
||||
# which leads to jobs observing stale data and not performing advancement.
|
||||
after_commit :after_finished_actions, on: :update, if: -> {
|
||||
saved_change_to_finished? ||
|
||||
saved_change_to_winner_id? ||
|
||||
saved_change_to_win_type? ||
|
||||
saved_change_to_score? ||
|
||||
saved_change_to_overtime_type?
|
||||
}
|
||||
|
||||
def after_finished_actions
|
||||
if self.w1
|
||||
wrestler1.touch
|
||||
end
|
||||
if self.w2
|
||||
wrestler2.touch
|
||||
end
|
||||
if self.finished == 1 && self.winner_id != nil
|
||||
if self.w1
|
||||
wrestler1.touch
|
||||
end
|
||||
if self.w2
|
||||
wrestler2.touch
|
||||
end
|
||||
if self.mat
|
||||
self.mat.assign_next_match
|
||||
end
|
||||
advance_wrestlers
|
||||
calculate_school_points
|
||||
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
|
||||
|
||||
BRACKET_POSITIONS = ["Pool","1/2","3/4","5/6","7/8","Quarter","Semis","Conso Semis","Bracket","Conso", "Conso Quarter"]
|
||||
WIN_TYPES = ["Decision", "Major", "Tech Fall", "Pin", "Forfeit", "Injury Default", "Default", "DQ", "BYE"]
|
||||
OVERTIME_TYPES = ["", "SV-1", "TB-1", "UTB", "SV-2", "TB-2", "OT"] # had to keep the blank here for validations
|
||||
|
||||
def score_validation
|
||||
if finished == 1
|
||||
if ! winner_id
|
||||
errors.add(:winner_id, "cannot be blank")
|
||||
end
|
||||
if win_type == "Pin" and ! score.match(/^[0-5]?[0-9]:[0-5][0-9]/)
|
||||
errors.add(:score, "needs to be in time format MM:SS when win type is Pin example: 2:23, 0:25, 10:03")
|
||||
end
|
||||
if win_type == "Decision" or win_type == "Tech Fall" or win_type == "Major" and ! score.match(/^[0-9]?[0-9]-[0-9]?[0-9]/)
|
||||
errors.add(:score, "needs to be in Number-Number format when win type is Decision, Tech Fall, and Major example: 10-2")
|
||||
end
|
||||
if (win_type == "Forfeit" or win_type == "Injury Default" or win_type == "Default" or win_type == "BYE" or win_type == "DQ") and (score != "")
|
||||
errors.add(:score, "needs to be blank when win type is Forfeit, Injury Default, Default, BYE, or DQ win_type")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def win_type_validation
|
||||
if finished == 1
|
||||
if ! WIN_TYPES.include? win_type
|
||||
errors.add(:win_type, "can only be one of the following #{WIN_TYPES.to_s}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def overtime_type_validation
|
||||
# overtime_type can be nil or of type OVERTIME_TYPES
|
||||
if overtime_type != nil and ! OVERTIME_TYPES.include? overtime_type
|
||||
errors.add(:overtime_type, "can only be one of the following #{OVERTIME_TYPES.to_s}")
|
||||
end
|
||||
end
|
||||
|
||||
def bracket_position_validation
|
||||
# Allow "Bracket Round of 16", "Bracket Round of 16.1",
|
||||
# "Conso Round of 8", "Conso Round of 8.2", etc.
|
||||
bracket_round_regex = /\A(Bracket|Conso) Round of \d+(\.\d+)?\z/
|
||||
|
||||
unless BRACKET_POSITIONS.include?(bracket_position) || bracket_position.match?(bracket_round_regex)
|
||||
errors.add(:bracket_position,
|
||||
"must be one of #{BRACKET_POSITIONS.to_s} " \
|
||||
"or match the pattern 'Bracket Round of X'/'Conso Round of X'")
|
||||
end
|
||||
end
|
||||
|
||||
def is_consolation_match
|
||||
if self.bracket_position == "Conso" or self.bracket_position == "Conso Quarter" or self.bracket_position == "Conso Semis" or self.bracket_position == "3/4" or self.bracket_position == "5/6" or self.bracket_position == "7/8"
|
||||
if self.bracket_position.include? "Conso" or self.bracket_position == "3/4" or self.bracket_position == "5/6" or self.bracket_position == "7/8"
|
||||
return true
|
||||
else
|
||||
return false
|
||||
@@ -33,7 +108,7 @@ class Match < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def is_championship_match
|
||||
if self.bracket_position == "Pool" or self.bracket_position == "Quarter" or self.bracket_position == "Semis" or self.bracket_position == "Bracket" or self.bracket_position == "1/2"
|
||||
if self.bracket_position == "Pool" or self.bracket_position == "Quarter" or self.bracket_position == "Semis" or self.bracket_position.include? "Bracket" or self.bracket_position == "1/2"
|
||||
return true
|
||||
else
|
||||
return false
|
||||
@@ -70,7 +145,7 @@ class Match < ActiveRecord::Base
|
||||
sec = time.partition(':').last.to_i
|
||||
return minutes_in_seconds + sec
|
||||
else
|
||||
nil
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
@@ -89,29 +164,23 @@ class Match < ActiveRecord::Base
|
||||
return ""
|
||||
end
|
||||
if self.finished == 1
|
||||
if self.win_type == "Default"
|
||||
return "(Def)"
|
||||
elsif self.win_type == "Injury Default"
|
||||
overtime_type_abbreviation = ""
|
||||
if self.overtime_type != "" and self.overtime_type
|
||||
overtime_type_abbreviation = " #{self.overtime_type}"
|
||||
end
|
||||
if self.win_type == "Injury Default"
|
||||
return "(Inj)"
|
||||
elsif self.win_type == "DQ"
|
||||
return "(DQ)"
|
||||
elsif self.win_type == "Forfeit"
|
||||
return "(For)"
|
||||
return "(FF)"
|
||||
else
|
||||
win_type_abbreviation = "#{self.win_type.chars.to_a[0..2].join('')}"
|
||||
return "(#{win_type_abbreviation} #{self.score})"
|
||||
return "(#{win_type_abbreviation} #{self.score}#{overtime_type_abbreviation})"
|
||||
end
|
||||
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
|
||||
@@ -129,52 +198,83 @@ class Match < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def w1_bracket_name
|
||||
if self.w1 != nil
|
||||
return "#{w1_name} (#{wrestler1.school.abbreviation})"
|
||||
else
|
||||
"#{w1_name}"
|
||||
first_round = first_round_for_weight
|
||||
return_string = ""
|
||||
return_string_ending = ""
|
||||
if self.w1 and self.winner_id == self.w1
|
||||
return_string = return_string + "<strong>"
|
||||
return_string_ending = return_string_ending + "</strong>"
|
||||
end
|
||||
if self.w1 != nil
|
||||
if self.round == first_round
|
||||
return_string = return_string + "#{wrestler1.long_bracket_name}"
|
||||
else
|
||||
return_string = return_string + "#{wrestler1.short_bracket_name}"
|
||||
end
|
||||
else
|
||||
return_string = return_string + "#{self.loser1_name}"
|
||||
end
|
||||
return return_string + return_string_ending
|
||||
end
|
||||
|
||||
def w2_bracket_name
|
||||
if self.w2 != nil
|
||||
return "#{w2_name} (#{wrestler2.school.abbreviation})"
|
||||
else
|
||||
"#{w2_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def w1_bracket_name_round_one
|
||||
if self.w1 != nil
|
||||
return "#{wrestler1.original_seed} #{w1_name} - #{wrestler1.school.abbreviation}"
|
||||
else
|
||||
"#{w1_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def w2_bracket_name_round_one
|
||||
if self.w2 != nil
|
||||
return "#{wrestler2.original_seed} #{w2_name} - #{wrestler2.school.abbreviation}"
|
||||
else
|
||||
"#{w2_name}"
|
||||
end
|
||||
first_round = first_round_for_weight
|
||||
return_string = ""
|
||||
return_string_ending = ""
|
||||
if self.w2 and self.winner_id == self.w2
|
||||
return_string = return_string + "<strong>"
|
||||
return_string_ending = return_string_ending + "</strong>"
|
||||
end
|
||||
if self.w2 != nil
|
||||
if self.round == first_round
|
||||
return_string = return_string + "#{wrestler2.long_bracket_name}"
|
||||
else
|
||||
return_string = return_string + "#{wrestler2.short_bracket_name}"
|
||||
end
|
||||
else
|
||||
return_string = return_string + "#{self.loser2_name}"
|
||||
end
|
||||
return return_string + return_string_ending
|
||||
end
|
||||
|
||||
def winner_name
|
||||
if self.finished != 1
|
||||
return ""
|
||||
end
|
||||
if self.winner_id == self.w1
|
||||
if self.winner == self.wrestler1
|
||||
return self.w1_name
|
||||
end
|
||||
if self.winner_id == self.w2
|
||||
if self.winner == self.wrestler2
|
||||
return self.w2_name
|
||||
end
|
||||
end
|
||||
|
||||
def all_results_text
|
||||
if self.finished != 1
|
||||
return ""
|
||||
end
|
||||
winning_wrestler = self.winner
|
||||
if winning_wrestler == self.wrestler1
|
||||
losing_wrestler = self.wrestler2
|
||||
elsif winning_wrestler == self.wrestler2
|
||||
losing_wrestler = self.wrestler1
|
||||
else
|
||||
# Handle cases where winner is not w1 or w2 (e.g., BYE, DQ where opponent might be nil)
|
||||
# Or maybe the match hasn't been fully populated yet after a win?
|
||||
# Returning an empty string for now, but this might need review based on expected scenarios.
|
||||
return ""
|
||||
end
|
||||
# Ensure losing_wrestler is not nil before accessing its properties
|
||||
losing_wrestler_name = losing_wrestler ? losing_wrestler.name : "Unknown"
|
||||
losing_wrestler_school = losing_wrestler ? losing_wrestler.school.name : "Unknown"
|
||||
|
||||
return "#{self.weight.max} lbs - #{winning_wrestler.name} (#{winning_wrestler.school.name}) #{self.win_type} #{losing_wrestler_name} (#{losing_wrestler_school}) #{self.score}"
|
||||
end
|
||||
|
||||
def bracket_winner_name
|
||||
if winner_name != ""
|
||||
return "#{winner_name} (#{Wrestler.find(winner_id).school.abbreviation})"
|
||||
# Use the winner association directly
|
||||
if self.winner
|
||||
return "#{self.winner.name} (#{self.winner.school.abbreviation})"
|
||||
else
|
||||
""
|
||||
end
|
||||
@@ -184,6 +284,17 @@ class Match < ActiveRecord::Base
|
||||
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
|
||||
@@ -227,4 +338,42 @@ class Match < ActiveRecord::Base
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_finished_at
|
||||
# Get the changes that will be persisted
|
||||
changes = changes_to_save
|
||||
|
||||
# Check if finished is changing from 0 to 1 or if it's already 1 but has no timestamp
|
||||
if (changes['finished'] && changes['finished'][1] == 1) || (finished == 1 && finished_at.nil?)
|
||||
self.finished_at = Time.current.utc
|
||||
end
|
||||
end
|
||||
|
||||
def broadcast_mat_assignment_change
|
||||
old_mat_id, new_mat_id = saved_change_to_mat_id || previous_changes["mat_id"]
|
||||
return unless old_mat_id || new_mat_id
|
||||
|
||||
[old_mat_id, new_mat_id].compact.uniq.each do |mat_id|
|
||||
mat = Mat.find_by(id: mat_id)
|
||||
next unless mat
|
||||
|
||||
Turbo::StreamsChannel.broadcast_update_to(
|
||||
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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class School < ActiveRecord::Base
|
||||
class School < ApplicationRecord
|
||||
belongs_to :tournament, touch: true
|
||||
has_many :wrestlers, dependent: :destroy
|
||||
has_many :deductedPoints, class_name: "Teampointadjust"
|
||||
has_many :delegates, class_name: "SchoolDelegate"
|
||||
has_many :deductedPoints, class_name: "Teampointadjust", dependent: :destroy
|
||||
has_many :delegates, class_name: "SchoolDelegate", dependent: :destroy
|
||||
|
||||
validates :name, presence: true
|
||||
|
||||
@@ -14,10 +14,15 @@ class School < ActiveRecord::Base
|
||||
|
||||
def abbreviation
|
||||
name_array = self.name.split(' ')
|
||||
if name_array.size > 1
|
||||
return "#{name_array[0].chars.to_a.first}#{name_array[1].chars.to_a[0..1].join('').upcase}"
|
||||
if name_array.size > 2
|
||||
# If three words, use first letter of first word, first letter of second, and first two of third
|
||||
return "#{name_array[0].chars.to_a.first}#{name_array[1].chars.to_a.first}#{name_array[2].chars.to_a[0..1].join('').upcase}"
|
||||
elsif name_array.size > 1
|
||||
# If two words use first letter of first word and first three of the second
|
||||
return "#{name_array[0].chars.to_a.first}#{name_array[1].chars.to_a[0..2].join('').upcase}"
|
||||
else
|
||||
return "#{name_array[0].chars.to_a[0..2].join('').upcase}"
|
||||
# If one word use first four letters
|
||||
return "#{name_array[0].chars.to_a[0..3].join('').upcase}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,40 +36,29 @@ class School < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def calculate_score
|
||||
if Rails.env.production?
|
||||
self.delay(:job_owner_id => self.tournament.id, :job_owner_type => "Calculate team score for #{self.name}").calculate_score_raw
|
||||
else
|
||||
calculate_score_raw
|
||||
end
|
||||
|
||||
end
|
||||
# Use perform_later which will execute based on centralized adapter config
|
||||
CalculateSchoolScoreJob.perform_later(self)
|
||||
end
|
||||
|
||||
def calculate_score_raw
|
||||
newScore = total_points_scored_by_wrestlers - total_points_deducted
|
||||
newScore = total_points_scored_by_wrestlers - total_points_deducted
|
||||
self.score = newScore
|
||||
self.save
|
||||
end
|
||||
|
||||
def total_points_scored_by_wrestlers
|
||||
points = 0
|
||||
points = 0.0
|
||||
self.wrestlers.each do |w|
|
||||
if w.extra != true
|
||||
points = points + w.total_team_points
|
||||
end
|
||||
points = points + w.total_team_points
|
||||
end
|
||||
points
|
||||
end
|
||||
|
||||
def total_points_deducted
|
||||
points = 0
|
||||
points = 0.0
|
||||
deductedPoints.each do |d|
|
||||
points = points + d.points
|
||||
end
|
||||
self.wrestlers.each do |w|
|
||||
w.deductedPoints.each do |d|
|
||||
points = points + d.points
|
||||
end
|
||||
end
|
||||
points
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class SchoolDelegate < ActiveRecord::Base
|
||||
class SchoolDelegate < ApplicationRecord
|
||||
belongs_to :school
|
||||
belongs_to :user
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Teampointadjust < ActiveRecord::Base
|
||||
belongs_to :wrestler, touch: true
|
||||
belongs_to :school, touch: true
|
||||
class Teampointadjust < ApplicationRecord
|
||||
belongs_to :wrestler, touch: true, optional: true
|
||||
belongs_to :school, touch: true, optional: true
|
||||
|
||||
after_save do
|
||||
advance_wrestlers_and_calc_team_score
|
||||
@@ -14,7 +14,7 @@ class Teampointadjust < ActiveRecord::Base
|
||||
#Team score needs calculated
|
||||
if self.wrestler_id != nil
|
||||
#In case this affects pool order
|
||||
AdvanceWrestler.new(self.wrestler).advance
|
||||
AdvanceWrestler.new(self.wrestler,self.wrestler.last_match).advance
|
||||
self.wrestler.school.calculate_score
|
||||
elsif self.school_id != nil
|
||||
self.school.calculate_score
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
class Tournament < ActiveRecord::Base
|
||||
class Tournament < ApplicationRecord
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :user, optional: true
|
||||
has_many :schools, dependent: :destroy
|
||||
has_many :weights, dependent: :destroy
|
||||
has_many :mats, dependent: :destroy
|
||||
has_many :wrestlers, through: :weights
|
||||
has_many :matches, dependent: :destroy
|
||||
has_many :delegates, class_name: "TournamentDelegate"
|
||||
has_many :delegates, class_name: "TournamentDelegate", dependent: :destroy
|
||||
has_many :mat_assignment_rules, dependent: :destroy
|
||||
has_many :tournament_backups, dependent: :destroy
|
||||
has_many :tournament_job_statuses, dependent: :destroy
|
||||
|
||||
validates :date, :name, :tournament_type, :address, :director, :director_email , presence: true
|
||||
|
||||
attr_accessor :import_text
|
||||
|
||||
def deferred_jobs
|
||||
Delayed::Job.where(job_owner_id: self.id)
|
||||
end
|
||||
|
||||
def self.search(search)
|
||||
where("date LIKE ? or name LIKE ?", "%#{search}%", "%#{search}%")
|
||||
def self.search_date_name(pattern)
|
||||
if pattern.blank? # blank? covers both nil and empty string
|
||||
all
|
||||
else
|
||||
search_functions = []
|
||||
search_variables = []
|
||||
search_terms = pattern.split(' ').map{|word| "%#{word.downcase}%"}
|
||||
search_terms.each do |word|
|
||||
search_functions << '(LOWER(name) LIKE ? or LOWER(date) LIKE ?)'
|
||||
# add twice for both ?'s in the function above
|
||||
search_variables << word
|
||||
search_variables << word
|
||||
end
|
||||
like_patterns = search_functions.join(' and ')
|
||||
# puts "where(#{like_patterns})"
|
||||
# puts *search_variables
|
||||
# example: (LOWER(name LIKE ? or LOWER(date) LIKE ?) and (LOWER(name) LIKE ? or LOWER(date) LIKE ?), %test%, %test%, %2016%, %2016%
|
||||
where("#{like_patterns}", *search_variables)
|
||||
end
|
||||
end
|
||||
|
||||
def days_until_start
|
||||
@@ -29,22 +45,59 @@ class Tournament < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def tournament_types
|
||||
["Pool to bracket","Modified 16 Man Double Elimination","Double Elimination 1-6"]
|
||||
["Pool to bracket","Modified 16 Man Double Elimination 1-6","Modified 16 Man Double Elimination 1-8","Regular Double Elimination 1-6","Regular Double Elimination 1-8"]
|
||||
end
|
||||
|
||||
def number_of_placers
|
||||
if self.tournament_type.include? "1-8"
|
||||
return 8
|
||||
elsif self.tournament_type.include? "1-6"
|
||||
return 6
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_all_team_scores
|
||||
self.schools.each do |school|
|
||||
school.calculate_score
|
||||
end
|
||||
end
|
||||
|
||||
def create_pre_defined_weights(weight_classes)
|
||||
weights.destroy_all
|
||||
weight_classes.each do |w|
|
||||
weights.create(max: w)
|
||||
end
|
||||
end
|
||||
|
||||
def create_pre_defined_weights(value)
|
||||
weights.destroy_all
|
||||
if value == 'hs'
|
||||
Weight::HS_WEIGHT_CLASSES.each do |w|
|
||||
weights.create(max: w)
|
||||
end
|
||||
else
|
||||
raise "Unspecified behavior"
|
||||
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)
|
||||
@@ -52,45 +105,30 @@ class Tournament < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def total_rounds
|
||||
if self.matches.count > 0
|
||||
self.matches.sort_by{|m| m.round}.last.round
|
||||
else
|
||||
0
|
||||
end
|
||||
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
|
||||
# Assuming this is line 147 that's causing the error
|
||||
matches.maximum(:round) || 0 # Return 0 if no matches or max round is nil
|
||||
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
|
||||
@@ -101,22 +139,24 @@ class Tournament < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def pool_to_bracket_weights_with_too_many_wrestlers
|
||||
if self.tournament_type == "Pool to bracket"
|
||||
weightsWithTooManyWrestlers = weights.select{|w| w.wrestlers.size > 24}
|
||||
if weightsWithTooManyWrestlers.size < 1
|
||||
return nil
|
||||
else
|
||||
return weightsWithTooManyWrestlers
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
def pool_to_bracket_number_of_wrestlers_error
|
||||
error_string = ""
|
||||
if self.tournament_type.include? "Pool to bracket"
|
||||
weights_with_too_many_wrestlers = weights.select{|w| w.wrestlers.size > 24}
|
||||
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 24 wrestlers."
|
||||
end
|
||||
weight_with_too_few_wrestlers.each do |weight|
|
||||
error_string = error_string + " The weight class #{weight.max} has less than 2 wrestlers."
|
||||
end
|
||||
end
|
||||
return error_string
|
||||
end
|
||||
|
||||
def modified_sixteen_man_number_of_wrestlers
|
||||
def modified_sixteen_man_number_of_wrestlers_error
|
||||
error_string = ""
|
||||
if self.tournament_type == "Modified 16 Man Double Elimination"
|
||||
if self.tournament_type.include? "Modified 16 Man Double Elimination"
|
||||
weights_with_too_many_wrestlers = weights.select{|w| w.wrestlers.size > 16}
|
||||
weight_with_too_few_wrestlers = weights.select{|w| w.wrestlers.size < 12}
|
||||
weights_with_too_many_wrestlers.each do |weight|
|
||||
@@ -128,23 +168,135 @@ class Tournament < ActiveRecord::Base
|
||||
end
|
||||
return error_string
|
||||
end
|
||||
|
||||
def double_elim_number_of_wrestlers_error
|
||||
error_string = ""
|
||||
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 < 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 2 wrestlers."
|
||||
end
|
||||
end
|
||||
return error_string
|
||||
end
|
||||
|
||||
def wrestlers_with_higher_seed_than_bracket_size_error
|
||||
error_string = ""
|
||||
weights.each do |weight|
|
||||
weight.wrestlers.each do |wrestler|
|
||||
if wrestler.original_seed != nil && wrestler.original_seed > weight.wrestlers.size
|
||||
error_string += "Wrestler: #{wrestler.name} has a seed of #{wrestler.original_seed} which is greater than the amount of wrestlers (#{weight.wrestlers.size}) in the weight class #{weight.max}."
|
||||
end
|
||||
end
|
||||
end
|
||||
return error_string
|
||||
end
|
||||
|
||||
def wrestlers_with_duplicate_original_seed_error
|
||||
error_string = ""
|
||||
weights.each do |weight|
|
||||
weight.wrestlers.select{|wr| wr.original_seed != nil}.each do |wrestler|
|
||||
if weight.wrestlers.select{|wr| wr.original_seed == wrestler.original_seed}.size > 1
|
||||
error_string += "More than 1 wrestler in the #{weight.max} weight class is seeded #{wrestler.original_seed}."
|
||||
end
|
||||
end
|
||||
end
|
||||
return error_string
|
||||
end
|
||||
|
||||
def match_generation_error
|
||||
errorString = "There is a tournament error."
|
||||
modified_sixteen_man_error = modified_sixteen_man_number_of_wrestlers
|
||||
if pool_to_bracket_weights_with_too_many_wrestlers != nil
|
||||
errorString = errorString + " The following weights have too many wrestlers "
|
||||
pool_to_bracket_weights_with_too_many_wrestlers.each do |w|
|
||||
errorString = errorString + "#{w.max} "
|
||||
end
|
||||
return errorString
|
||||
elsif modified_sixteen_man_error.length > 0
|
||||
return errorString + modified_sixteen_man_error
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
def wrestlers_with_out_of_order_seed_error
|
||||
error_string = ""
|
||||
weights.each do |weight|
|
||||
original_seeds = weight.wrestlers.map(&:original_seed).compact.sort
|
||||
if original_seeds.any? && original_seeds != (original_seeds.first..original_seeds.last).to_a
|
||||
error_string += "The weight class #{weight.max} has wrestlers with out-of-order seeds: #{original_seeds}. There is a gap in the sequence."
|
||||
end
|
||||
end
|
||||
return error_string
|
||||
end
|
||||
|
||||
def match_generation_error
|
||||
error_string = ""
|
||||
if pool_to_bracket_number_of_wrestlers_error.length > 0
|
||||
error_string += pool_to_bracket_number_of_wrestlers_error
|
||||
elsif modified_sixteen_man_number_of_wrestlers_error.length > 0
|
||||
error_string += modified_sixteen_man_number_of_wrestlers_error
|
||||
elsif double_elim_number_of_wrestlers_error.length > 0
|
||||
error_string += double_elim_number_of_wrestlers_error
|
||||
elsif wrestlers_with_higher_seed_than_bracket_size_error.length > 0
|
||||
error_string += wrestlers_with_higher_seed_than_bracket_size_error
|
||||
elsif wrestlers_with_duplicate_original_seed_error.length > 0
|
||||
error_string += wrestlers_with_duplicate_original_seed_error
|
||||
elsif wrestlers_with_out_of_order_seed_error.length > 0
|
||||
error_string += wrestlers_with_out_of_order_seed_error
|
||||
end
|
||||
if error_string.length > 0
|
||||
return "There is a tournament error. #{error_string}"
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def reset_and_fill_bout_board
|
||||
reset_mats
|
||||
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
|
||||
break unless assigned_any
|
||||
end
|
||||
end
|
||||
|
||||
def create_backup()
|
||||
TournamentBackupService.new(self, "Manual backup").create_backup
|
||||
end
|
||||
|
||||
def confirm_all_weights_have_original_seeds
|
||||
error_string = wrestlers_with_higher_seed_than_bracket_size_error
|
||||
error_string += wrestlers_with_duplicate_original_seed_error
|
||||
error_string += wrestlers_with_out_of_order_seed_error
|
||||
|
||||
return error_string.blank?
|
||||
end
|
||||
|
||||
def confirm_each_weight_class_has_correct_number_of_wrestlers
|
||||
error_string = pool_to_bracket_number_of_wrestlers_error
|
||||
error_string += modified_sixteen_man_number_of_wrestlers_error
|
||||
error_string += double_elim_number_of_wrestlers_error
|
||||
|
||||
return error_string.blank?
|
||||
end
|
||||
|
||||
# Check if there are any active jobs for this tournament
|
||||
def has_active_jobs?
|
||||
tournament_job_statuses.active.exists?
|
||||
end
|
||||
|
||||
# Get all active jobs for this tournament
|
||||
def active_jobs
|
||||
tournament_job_statuses.active
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def connection_adapter
|
||||
ActiveRecord::Base.connection.adapter_name
|
||||
end
|
||||
end
|
||||
|
||||
5
app/models/tournament_backup.rb
Normal file
5
app/models/tournament_backup.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class TournamentBackup < ApplicationRecord
|
||||
belongs_to :tournament
|
||||
|
||||
validates :backup_data, presence: true
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class TournamentDelegate < ActiveRecord::Base
|
||||
class TournamentDelegate < ApplicationRecord
|
||||
belongs_to :tournament
|
||||
belongs_to :user
|
||||
end
|
||||
|
||||
22
app/models/tournament_job_status.rb
Normal file
22
app/models/tournament_job_status.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
class TournamentJobStatus < ApplicationRecord
|
||||
belongs_to :tournament, optional: false
|
||||
|
||||
# Validations
|
||||
validates :job_name, presence: true
|
||||
validates :status, presence: true
|
||||
validates_inclusion_of :status, in: ["Queued", "Running", "Errored"], allow_nil: false
|
||||
validates :tournament, presence: true
|
||||
|
||||
# Scopes
|
||||
scope :active, -> { where.not(status: "Errored") }
|
||||
|
||||
# Class methods to find jobs for a tournament
|
||||
def self.for_tournament(tournament)
|
||||
where(tournament_id: tournament.id)
|
||||
end
|
||||
|
||||
# Clean up completed jobs (should be called when job finishes successfully)
|
||||
def self.complete_job(tournament_id, job_name)
|
||||
where(tournament_id: tournament_id, job_name: job_name).destroy_all
|
||||
end
|
||||
end
|
||||
@@ -1,27 +1,68 @@
|
||||
class User < ActiveRecord::Base
|
||||
class User < ApplicationRecord
|
||||
attr_accessor :reset_token
|
||||
|
||||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable and :omniauthable
|
||||
has_many :tournaments
|
||||
has_many :delegated_tournament_permissions, class_name: "TournamentDelegate"
|
||||
has_many :delegated_school_permissions, class_name: "SchoolDelegate"
|
||||
has_many :delegated_tournament_permissions, class_name: "TournamentDelegate", dependent: :destroy
|
||||
has_many :delegated_school_permissions, class_name: "SchoolDelegate", dependent: :destroy
|
||||
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :trackable, :validatable
|
||||
# Replace Devise with has_secure_password
|
||||
has_secure_password
|
||||
|
||||
# Add validations that were handled by Devise
|
||||
validates :email, presence: true, uniqueness: { case_sensitive: false }
|
||||
validates :password, length: { minimum: 6 }, allow_nil: true
|
||||
|
||||
# These are the Devise modules we were using:
|
||||
# devise :database_authenticatable, :registerable,
|
||||
# :recoverable, :rememberable, :trackable, :validatable
|
||||
|
||||
# Returns the hash digest of the given string
|
||||
def self.digest(string)
|
||||
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
||||
BCrypt::Password.create(string, cost: cost)
|
||||
end
|
||||
|
||||
# Returns a random token
|
||||
def self.new_token
|
||||
SecureRandom.urlsafe_base64
|
||||
end
|
||||
|
||||
# Sets the password reset attributes
|
||||
def create_reset_digest
|
||||
self.reset_token = User.new_token
|
||||
update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
|
||||
end
|
||||
|
||||
# Sends password reset email
|
||||
def send_password_reset_email
|
||||
UserMailer.password_reset(self).deliver_now
|
||||
end
|
||||
|
||||
# Returns true if a password reset has expired
|
||||
def password_reset_expired?
|
||||
reset_sent_at < 2.hours.ago
|
||||
end
|
||||
|
||||
# Returns true if the given token matches the digest
|
||||
def authenticated?(attribute, token)
|
||||
digest = send("#{attribute}_digest")
|
||||
return false if digest.nil?
|
||||
BCrypt::Password.new(digest).is_password?(token)
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Weight < ActiveRecord::Base
|
||||
belongs_to :tournament
|
||||
class Weight < ApplicationRecord
|
||||
belongs_to :tournament, touch: true
|
||||
has_many :wrestlers, dependent: :destroy
|
||||
has_many :matches, dependent: :destroy
|
||||
|
||||
@@ -7,7 +7,13 @@ class Weight < ActiveRecord::Base
|
||||
|
||||
validates :max, presence: true
|
||||
|
||||
HS_WEIGHT_CLASSES = [106,113,120,126,132,138,145,152,160,170,182,195,220,285]
|
||||
# passed via layouts/_tournament-navbar.html.erb
|
||||
# tournaments controller does a .split(',') on this string and creates an array via commas
|
||||
# tournament model runs the code via method create_pre_defined_weights
|
||||
HS_WEIGHT_CLASSES = "106,113,120,126,132,138,144,150,157,165,175,190,215,285"
|
||||
HS_GIRLS_WEIGHT_CLASSES = "100,105,110,115,120,125,130,135,140,145,155,170,190,235"
|
||||
MS_WEIGHT_CLASSES = "80,86,92,98,104,110,116,122,128,134,142,150,160,172,205,245"
|
||||
MS_GIRLS_WEIGHT_CLASSES = "72,80,86,92,98,104,110,116,122,128,134,142,155,170,190,235"
|
||||
|
||||
before_destroy do
|
||||
self.tournament.destroy_all_matches
|
||||
@@ -148,5 +154,48 @@ class Weight < ActiveRecord::Base
|
||||
def wrestlers_without_pool_assignment
|
||||
wrestlers.select{|w| w.pool == nil}
|
||||
end
|
||||
|
||||
|
||||
def calculate_bracket_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
|
||||
2**Math.log2(num_wrestlers).ceil
|
||||
end
|
||||
|
||||
def highest_bracket_round
|
||||
bracket_matches_sorted_by_round_descending = matches.select{|m| m.bracket_position.include? "Bracket"}.sort_by{|m| m.round}.reverse
|
||||
if bracket_matches_sorted_by_round_descending.size > 0
|
||||
return bracket_matches_sorted_by_round_descending.first.round
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def lowest_bracket_round
|
||||
bracket_matches_sorted_by_round_ascending = matches.select{|m| m.bracket_position.include? "Bracket"}.sort_by{|m| m.round}
|
||||
if bracket_matches_sorted_by_round_ascending.size > 0
|
||||
return bracket_matches_sorted_by_round_ascending.first.round
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def highest_conso_round
|
||||
conso_matches_sorted_by_round_descending = matches.select{|m| m.bracket_position.include? "Conso"}.sort_by{|m| m.round}.reverse
|
||||
if conso_matches_sorted_by_round_descending.size > 0
|
||||
return conso_matches_sorted_by_round_descending.first.round
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def lowest_conso_round
|
||||
conso_matches_sorted_by_round_ascending = matches.select{|m| m.bracket_position.include? "Conso"}.sort_by{|m| m.round}
|
||||
if conso_matches_sorted_by_round_ascending.size > 0
|
||||
return conso_matches_sorted_by_round_ascending.first.round
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
class Wrestler < ActiveRecord::Base
|
||||
class Wrestler < ApplicationRecord
|
||||
belongs_to :school, touch: true
|
||||
belongs_to :weight, touch: true
|
||||
has_one :tournament, through: :weight
|
||||
has_many :matches, through: :weight
|
||||
has_many :deductedPoints, class_name: "Teampointadjust"
|
||||
has_many :deductedPoints, class_name: "Teampointadjust", dependent: :destroy
|
||||
## Matches association
|
||||
# Rails associations expect only a single column so we cannot do a w1 OR w2
|
||||
# So we have to create two associations and combine them with the all_matches method
|
||||
has_many :matches_as_w1, ->(wrestler){ where(weight_id: wrestler.weight_id) }, class_name: 'Match', foreign_key: 'w1'
|
||||
has_many :matches_as_w2, ->(wrestler){ where(weight_id: wrestler.weight_id) }, class_name: 'Match', foreign_key: 'w2'
|
||||
##
|
||||
attr_accessor :poolAdvancePoints, :originalId, :swapId
|
||||
|
||||
validates :name, :weight_id, :school_id, presence: true
|
||||
@@ -18,7 +23,7 @@ class Wrestler < ActiveRecord::Base
|
||||
|
||||
|
||||
def last_finished_match
|
||||
all_matches.select{|m| m.finished == 1}.sort_by{|m| m.bout_number}.last
|
||||
all_matches.select{|m| m.finished == 1}.sort_by{|m| m.finished_at}.last
|
||||
end
|
||||
|
||||
def total_team_points
|
||||
@@ -38,7 +43,7 @@ class Wrestler < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def total_pool_points_for_pool_order
|
||||
CalculateWrestlerTeamScore.new(self).poolPoints + CalculateWrestlerTeamScore.new(self).bonusWinPoints
|
||||
CalculateWrestlerTeamScore.new(self).poolPoints + CalculateWrestlerTeamScore.new(self).pool_bonus_points
|
||||
end
|
||||
|
||||
def unfinished_pool_matches
|
||||
@@ -59,7 +64,7 @@ class Wrestler < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def winner_of_last_match?
|
||||
if last_match.winner_id == self.id
|
||||
if last_match && last_match.winner == self # Keep winner association change
|
||||
return true
|
||||
else
|
||||
return false
|
||||
@@ -85,17 +90,30 @@ class Wrestler < ActiveRecord::Base
|
||||
def unfinished_matches
|
||||
all_matches.select{|m| m.finished != 1}.sort_by{|m| m.bout_number}
|
||||
end
|
||||
|
||||
|
||||
def result_by_bout(bout)
|
||||
bout_match = all_matches.select{|m| m.bout_number == bout and m.finished == 1}
|
||||
if bout_match.size == 0
|
||||
bout_match_results = all_matches.select{|m| m.bout_number == bout and m.finished == 1}
|
||||
if bout_match_results.empty?
|
||||
return ""
|
||||
end
|
||||
if bout_match.first.winner_id == self.id
|
||||
return "W #{bout_match.first.bracket_score_string}"
|
||||
bout_match = bout_match_results.first
|
||||
if bout_match.winner == self # Keep winner association change
|
||||
return "W #{bout_match.bracket_score_string}"
|
||||
else
|
||||
return "L #{bout_match.bracket_score_string}"
|
||||
end
|
||||
if bout_match.first.winner_id != self.id
|
||||
return "L #{bout_match.first.bracket_score_string}"
|
||||
end
|
||||
|
||||
def result_by_id(id)
|
||||
bout_match_results = all_matches.select{|m| m.id == id and m.finished == 1}
|
||||
if bout_match_results.empty?
|
||||
return ""
|
||||
end
|
||||
bout_match = bout_match_results.first
|
||||
if bout_match.winner == self # Keep winner association change
|
||||
return "W #{bout_match.bracket_score_string}"
|
||||
else
|
||||
return "L #{bout_match.bracket_score_string}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -107,7 +125,8 @@ class Wrestler < ActiveRecord::Base
|
||||
if all_matches.blank?
|
||||
return false
|
||||
else
|
||||
return true
|
||||
# Original logic checked blank?, not specific round. Reverting to that.
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -119,9 +138,22 @@ class Wrestler < ActiveRecord::Base
|
||||
return round_match.bout_number
|
||||
end
|
||||
end
|
||||
|
||||
def match_id_by_round(round)
|
||||
round_match = all_matches.select{|m| m.round == round}.first
|
||||
if round_match.blank?
|
||||
return "BYE"
|
||||
else
|
||||
return round_match.id
|
||||
end
|
||||
end
|
||||
|
||||
# Restore all_matches method
|
||||
def all_matches
|
||||
return matches.select{|m| m.w1 == self.id or m.w2 == self.id}
|
||||
# Combine the two specific associations.
|
||||
# This returns an Array, similar to the previous select method.
|
||||
# Add .uniq for safety and sort for consistent order.
|
||||
(matches_as_w1 + matches_as_w2).uniq.sort_by(&:bout_number)
|
||||
end
|
||||
|
||||
def pool_matches
|
||||
@@ -130,7 +162,9 @@ class Wrestler < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def has_a_pool_bye
|
||||
if weight.pool_rounds(matches) > pool_matches.size
|
||||
# Revert back to using all_matches here too? Seems complex.
|
||||
# Sticking with original: uses `matches` (all weight) and `pool_matches` (derived from all_matches)
|
||||
if weight.pool_rounds(all_matches) > pool_matches.size
|
||||
return true
|
||||
else
|
||||
return false
|
||||
@@ -138,11 +172,19 @@ class Wrestler < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def championship_advancement_wins
|
||||
matches_won.select{|m| (m.bracket_position == "Quarter" or m.bracket_position == "Semis" or m.bracket_position == "Bracket") and m.win_type != "BYE"}
|
||||
matches_won.select{|m| (m.bracket_position == "Quarter" or m.bracket_position == "Semis" or m.bracket_position.include? "Bracket") and m.win_type != "BYE"}
|
||||
end
|
||||
|
||||
def consolation_advancement_wins
|
||||
matches_won.select{|m| (m.bracket_position == "Conso Semis" or m.bracket_position == "Conso" or m.bracket_position == "Conso Quarter") and m.win_type != "BYE"}
|
||||
matches_won.select{|m| (m.bracket_position.include? "Conso") and m.win_type != "BYE"}
|
||||
end
|
||||
|
||||
def championship_byes
|
||||
matches_won.select{|m| (m.bracket_position == "Quarter" or m.bracket_position == "Semis" or m.bracket_position.include? "Bracket") and m.win_type == "BYE"}
|
||||
end
|
||||
|
||||
def consolation_byes
|
||||
matches_won.select{|m| (m.bracket_position.include? "Conso") and m.win_type == "BYE"}
|
||||
end
|
||||
|
||||
def finished_matches
|
||||
@@ -158,7 +200,8 @@ class Wrestler < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def matches_won
|
||||
all_matches.select{|m| m.winner_id == self.id}
|
||||
# Revert, but keep using winner association check
|
||||
all_matches.select{|m| m.winner == self}
|
||||
end
|
||||
|
||||
def pool_wins
|
||||
@@ -196,9 +239,28 @@ class Wrestler < ActiveRecord::Base
|
||||
points_scored
|
||||
end
|
||||
|
||||
def decision_points_scored_pool
|
||||
points_scored = 0
|
||||
decision_wins.select{|m| m.bracket_position == "Pool"}.each do |m|
|
||||
score_of_match = m.score.delete(" ")
|
||||
score_one = score_of_match.partition('-').first.to_i
|
||||
score_two = score_of_match.partition('-').last.to_i
|
||||
if score_one > score_two
|
||||
points_scored = points_scored + score_one
|
||||
elsif score_two > score_one
|
||||
points_scored = points_scored + score_two
|
||||
end
|
||||
end
|
||||
points_scored
|
||||
end
|
||||
|
||||
def fastest_pin
|
||||
pin_wins.sort_by{|m| m.pin_time_in_seconds}.first
|
||||
end
|
||||
|
||||
def fastest_pin_pool
|
||||
pin_wins.select{|m| m.bracket_position == "Pool"}.sort_by{|m| m.pin_time_in_seconds}.first
|
||||
end
|
||||
|
||||
def pin_time
|
||||
time = 0
|
||||
@@ -208,14 +270,28 @@ class Wrestler < ActiveRecord::Base
|
||||
time
|
||||
end
|
||||
|
||||
def pin_time_pool
|
||||
time = 0
|
||||
pin_wins.select{|m| m.bracket_position == "Pool"}.each do | m |
|
||||
time = time + m.pin_time_in_seconds
|
||||
end
|
||||
time
|
||||
end
|
||||
|
||||
def season_win_percentage
|
||||
win = self.season_win.to_f
|
||||
loss = self.season_loss.to_f
|
||||
# Revert to original logic
|
||||
if win > 0 and loss != nil
|
||||
match_total = win + loss
|
||||
percentage_dec = win / match_total
|
||||
percentage = percentage_dec * 100
|
||||
return percentage.to_i
|
||||
if match_total > 0
|
||||
percentage_dec = win / match_total
|
||||
percentage = percentage_dec * 100
|
||||
return percentage.to_i
|
||||
else
|
||||
# Avoid division by zero if somehow win > 0 but total <= 0
|
||||
return 0
|
||||
end
|
||||
elsif self.season_win == 0
|
||||
return 0
|
||||
elsif self.season_win == nil or self.season_loss == nil
|
||||
@@ -223,4 +299,26 @@ class Wrestler < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def long_bracket_name
|
||||
# Revert to original logic
|
||||
return_string = ""
|
||||
if self.original_seed
|
||||
return_string = return_string + "[#{self.original_seed}] "
|
||||
end
|
||||
return_string = return_string + "#{self.name} - #{self.school.name}"
|
||||
if self.season_win && self.season_loss
|
||||
return_string = return_string + " (#{self.season_win}-#{self.season_loss})"
|
||||
end
|
||||
return return_string
|
||||
end
|
||||
|
||||
def short_bracket_name
|
||||
# Revert to original logic
|
||||
return "#{self.name} (#{self.school.abbreviation})"
|
||||
end
|
||||
|
||||
def name_with_school
|
||||
# Revert to original logic
|
||||
return "#{self.name} - #{self.school.name}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,29 +1,108 @@
|
||||
class AdvanceWrestler
|
||||
def initialize( wrestler, last_match )
|
||||
def initialize(wrestler, last_match)
|
||||
@wrestler = wrestler
|
||||
@tournament = @wrestler.tournament
|
||||
@last_match = last_match
|
||||
end
|
||||
|
||||
def advance
|
||||
if Rails.env.production?
|
||||
self.delay(:job_owner_id => @tournament.id, :job_owner_type => "Advance wrestler #{@wrestler.name} in the bracket").advance_raw
|
||||
else
|
||||
advance_raw
|
||||
end
|
||||
# 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, @tournament.id)
|
||||
end
|
||||
|
||||
def advance_raw
|
||||
pool_to_bracket_advancement if @tournament.tournament_type == "Pool to bracket"
|
||||
DoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type == "Modified 16 Man Double Elimination" or
|
||||
@tournament.tournament_type == "Double Elimination 1-6"
|
||||
@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
|
||||
|
||||
@@ -1,86 +1,78 @@
|
||||
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
|
||||
|
||||
def bracket_advancement
|
||||
if @last_match.winner_id == @wrestler.id
|
||||
winner_advance
|
||||
end
|
||||
if @last_match.winner_id != @wrestler.id
|
||||
loser_advance
|
||||
end
|
||||
advance_wrestler
|
||||
advance_double_byes
|
||||
set_bye_for_placement
|
||||
end
|
||||
|
||||
def winner_advance
|
||||
def advance_wrestler
|
||||
# Advance winner
|
||||
if @last_match.winner == @wrestler
|
||||
winners_bracket_advancement
|
||||
# Advance loser
|
||||
elsif @last_match.winner != @wrestler
|
||||
losers_bracket_advancement
|
||||
end
|
||||
end
|
||||
|
||||
def winners_bracket_advancement
|
||||
if (@last_match.loser1_name == "BYE" or @last_match.loser2_name == "BYE") and @last_match.is_championship_match
|
||||
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
|
||||
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
|
||||
update_new_match(new_match, get_wrestler_number)
|
||||
elsif @last_match.bracket_position == "Conso Semis"
|
||||
# if its a regular double elim
|
||||
if Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Conso Semis",@next_match_position_number.ceil,@wrestler.weight_id).first.loser1_name != nil
|
||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","3/4",@next_match_position_number.ceil,@wrestler.weight_id).first
|
||||
update_new_match(new_match, get_wrestler_number)
|
||||
# if it's a special bracket where consolations wrestler for 5th
|
||||
else
|
||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","5/6",@next_match_position_number.ceil,@wrestler.weight_id).first
|
||||
update_new_match(new_match, get_wrestler_number)
|
||||
end
|
||||
elsif @last_match.bracket_position == "Conso Quarter"
|
||||
next_round_matches = Match.where("weight_id = ? and bracket_position = ?", @wrestler.weight_id, "Conso Semis").sort_by{|m| m.round}
|
||||
this_round_matches = Match.where("weight_id = ? and round = ? and bracket_position = ?", @wrestler.weight_id, @last_match.round, "Conso Quarter")
|
||||
if next_round_matches.size == this_round_matches.size
|
||||
# if a semi loser is dropping down
|
||||
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Conso Semis",@last_match.bracket_position_number,@wrestler.weight_id).first
|
||||
update_new_match(new_match,2)
|
||||
else
|
||||
# if 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
|
||||
update_new_match(new_match, get_wrestler_number)
|
||||
end
|
||||
elsif @last_match.bracket_position == "Bracket"
|
||||
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and (bracket_position = ? or bracket_position = ?)", @next_match_position_number.ceil,@wrestler.weight_id, @last_match.round , "Bracket", "Quarter").sort_by{|m| m.round}.first
|
||||
update_new_match(new_match, get_wrestler_number)
|
||||
elsif @last_match.bracket_position == "Conso"
|
||||
next_round = next_round_matches = Match.where("weight_id = ? and round > ? and (bracket_position = ? or bracket_position = ?)", @wrestler.weight_id, @last_match.round, "Conso", "Conso Quarter").sort_by{|m| m.round}.first.round
|
||||
next_round_matches = Match.where("weight_id = ? and round = ? and (bracket_position = ? or bracket_position = ?)", @wrestler.weight_id, next_round, "Conso", "Conso Quarter")
|
||||
this_round_matches = Match.where("weight_id = ? and round = ? and (bracket_position = ? or bracket_position = ?)", @wrestler.weight_id, @last_match.round, "Conso", "Conso Quarter")
|
||||
# if a loser is dropping down
|
||||
if next_round_matches.size == this_round_matches.size
|
||||
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and (bracket_position = ? or bracket_position = ?)", @last_match.bracket_position_number,@wrestler.weight_id, @last_match.round, "Conso", "Conso Quarter").sort_by{|m| m.round}.first
|
||||
update_new_match(new_match, 2)
|
||||
else
|
||||
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and (bracket_position = ? or bracket_position = ?)", @next_match_position_number.ceil,@wrestler.weight_id, @last_match.round, "Conso", "Conso Quarter").sort_by{|m| m.round}.first
|
||||
update_new_match(new_match, get_wrestler_number)
|
||||
end
|
||||
|
||||
future_round_matches = @last_match.weight.matches.select{|m| m.round > @last_match.round}
|
||||
next_match = nil
|
||||
next_match_bracket_position = nil
|
||||
next_match_position_number = @next_match_position_number.ceil
|
||||
|
||||
if @last_match.is_championship_match and future_round_matches.size > 0
|
||||
next_match_round = future_round_matches.select{|m| m.is_championship_match}.sort_by{|m| m.round}.first.round
|
||||
next_match_bracket_position = future_round_matches.select{|m| m.is_championship_match}.sort_by{|m| m.round}.first.bracket_position
|
||||
end
|
||||
|
||||
if @last_match.is_consolation_match and future_round_matches.size > 0
|
||||
next_match_round = future_round_matches.select{|m| m.is_consolation_match}.sort_by{|m| m.round}.first.round
|
||||
next_match_bracket_position = future_round_matches.select{|m| m.is_consolation_match}.sort_by{|m| m.round}.first.bracket_position
|
||||
next_match_loser1_name = future_round_matches.select{|m| m.is_consolation_match}.sort_by{|m| m.round}.first.loser1_name
|
||||
# If someone is falling down to them in this round, then their bracket_position_number stays the same
|
||||
if next_match_loser1_name
|
||||
next_match_position_number = @last_match.bracket_position_number
|
||||
end
|
||||
end
|
||||
|
||||
if next_match_bracket_position
|
||||
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
|
||||
update_new_match(next_match, get_wrestler_number)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -92,46 +84,90 @@ class DoubleEliminationAdvance
|
||||
end
|
||||
end
|
||||
|
||||
def loser_advance
|
||||
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)
|
||||
if next_match.size > 0
|
||||
next_match = next_match.first
|
||||
next_match.replace_loser_name_with_wrestler(@wrestler,"Loser of #{bout}")
|
||||
if next_match.loser1_name == "BYE" or next_match.loser2_name == "BYE"
|
||||
next_match.winner_id = @wrestler.id
|
||||
next_match.win_type = "BYE"
|
||||
next_match.finished = 1
|
||||
next_match.save
|
||||
next_match.advance_wrestlers
|
||||
end
|
||||
def losers_bracket_advancement
|
||||
bout = @last_match.bout_number
|
||||
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?
|
||||
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
|
||||
next_match.finished_at = Time.current
|
||||
@matches_to_advance << next_match
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
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 = @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 = @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"
|
||||
replace_loser_name_with_bye(seventh_finals, "Loser of #{match.bout_number}")
|
||||
end
|
||||
end
|
||||
end
|
||||
if fifth_finals
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
class ModifiedDoubleEliminationAdvance
|
||||
|
||||
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
|
||||
|
||||
def bracket_advancement
|
||||
advance_wrestler
|
||||
advance_double_byes
|
||||
set_bye_for_placement
|
||||
end
|
||||
|
||||
def advance_wrestler
|
||||
if @last_match.winner == @wrestler
|
||||
winners_bracket_advancement
|
||||
elsif @last_match.winner != @wrestler
|
||||
losers_bracket_advancement
|
||||
end
|
||||
end
|
||||
|
||||
def winners_bracket_advancement
|
||||
if (@last_match.loser1_name == "BYE" or @last_match.loser2_name == "BYE") and @last_match.is_championship_match
|
||||
update_consolation_bye
|
||||
end
|
||||
if @last_match.bracket_position == "Quarter"
|
||||
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 = @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 = @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 = @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 = @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 = @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
|
||||
elsif wrestler_number == 1
|
||||
match.w1 = @wrestler.id
|
||||
end
|
||||
end
|
||||
|
||||
def update_consolation_bye
|
||||
bout = @wrestler.last_match.bout_number
|
||||
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
|
||||
|
||||
def get_wrestler_number
|
||||
if @next_match_position_number != @next_match_position_number.ceil
|
||||
return 1
|
||||
elsif @next_match_position_number == @next_match_position_number.ceil
|
||||
return 2
|
||||
end
|
||||
end
|
||||
|
||||
def losers_bracket_advancement
|
||||
bout = @last_match.bout_number
|
||||
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?
|
||||
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
|
||||
next_match.finished_at = Time.current
|
||||
@matches_to_advance << next_match
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def advance_double_byes
|
||||
weight = @wrestler.weight
|
||||
@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 = @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" 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 && next_match.bracket_position_number == next_match_position_number
|
||||
next_match.loser2_name = "BYE"
|
||||
elsif next_match
|
||||
next_match.loser1_name = "BYE"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_bye_for_placement
|
||||
weight = @wrestler.weight
|
||||
seventh_finals = @matches.select{|match| match.weight_id == weight.id && match.bracket_position == '7/8'}.first
|
||||
if seventh_finals
|
||||
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"
|
||||
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
|
||||
|
||||
@@ -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,59 +22,68 @@ 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
|
||||
|
||||
def bracketAdvancment
|
||||
if @last_match.winner_id == @wrestler.id
|
||||
winnerAdvance
|
||||
end
|
||||
if @last_match.winner_id != @wrestler.id
|
||||
loserAdvance
|
||||
end
|
||||
advance_wrestlers
|
||||
end
|
||||
|
||||
def winnerAdvance
|
||||
def advance_wrestlers
|
||||
# Advance winner
|
||||
if @last_match.winner == @wrestler
|
||||
winner_advance
|
||||
# Advance loser
|
||||
elsif @last_match.winner != @wrestler
|
||||
loser_advance
|
||||
end
|
||||
end
|
||||
|
||||
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 loserAdvance
|
||||
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
|
||||
|
||||
@@ -5,17 +5,25 @@ class PoolOrder
|
||||
|
||||
def getPoolOrder
|
||||
setOriginalPoints
|
||||
while checkForTieBreakers == true
|
||||
breakTie
|
||||
while checkForTies(@wrestlers) == true
|
||||
getWrestlersOrderByPoolAdvancePoints.each do |wrestler|
|
||||
wrestlers_with_same_points = getWrestlersOrderByPoolAdvancePoints.select{|w| w.poolAdvancePoints == wrestler.poolAdvancePoints}
|
||||
if wrestlers_with_same_points.size > 1
|
||||
breakTie(wrestlers_with_same_points)
|
||||
end
|
||||
end
|
||||
end
|
||||
@wrestlers.sort_by{|w| w.poolAdvancePoints}.reverse.each_with_index do |wrestler, index|
|
||||
placement = index + 1
|
||||
wrestler.pool_placement = placement
|
||||
wrestler.save
|
||||
getWrestlersOrderByPoolAdvancePoints.each_with_index do |wrestler, index|
|
||||
placement = index + 1
|
||||
wrestler.pool_placement = placement
|
||||
end
|
||||
@wrestlers.sort_by{|w| w.poolAdvancePoints}.reverse!
|
||||
end
|
||||
|
||||
def getWrestlersOrderByPoolAdvancePoints
|
||||
@wrestlers.sort_by{|w| w.poolAdvancePoints}.reverse
|
||||
end
|
||||
|
||||
def setOriginalPoints
|
||||
@wrestlers.each do |w|
|
||||
w.pool_placement_tiebreaker = nil
|
||||
@@ -24,16 +32,16 @@ class PoolOrder
|
||||
end
|
||||
end
|
||||
|
||||
def checkForTieBreakers
|
||||
if wrestlersWithSamePoints.size > 1
|
||||
def checkForTies(wrestlers_to_check)
|
||||
if wrestlersWithSamePoints(wrestlers_to_check).size > 1
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
def wrestlersWithSamePoints
|
||||
@wrestlers.each do |w|
|
||||
wrestlersWithSamePointsLocal = @wrestlers.select{|wr| wr.poolAdvancePoints == w.poolAdvancePoints}
|
||||
def wrestlersWithSamePoints(wrestlers_to_check)
|
||||
wrestlers_to_check.each do |w|
|
||||
wrestlersWithSamePointsLocal = wrestlers_to_check.select{|wr| wr.poolAdvancePoints == w.poolAdvancePoints}
|
||||
if wrestlersWithSamePointsLocal.size > 1
|
||||
return wrestlersWithSamePointsLocal
|
||||
end
|
||||
@@ -41,42 +49,47 @@ class PoolOrder
|
||||
return []
|
||||
end
|
||||
|
||||
def ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
if wrestlersWithSamePoints.size == originalTieSize
|
||||
def ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_to_check)
|
||||
if wrestlersWithSamePoints(wrestlers_to_check).size == originalTieSize
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def breakTie
|
||||
originalTieSize = wrestlersWithSamePoints.size
|
||||
deductedPoints(originalTieSize) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
def breakTie(wrestlers_with_same_points)
|
||||
originalTieSize = wrestlers_with_same_points.size
|
||||
deductedPoints(originalTieSize,wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
if originalTieSize == 2
|
||||
headToHead if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
headToHead(wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
end
|
||||
teamPoints if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
mostFalls if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
mostTechs if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
mostMajors if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
mostDecisionPointsScored if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
fastest_pins if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
coinFlip if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize)
|
||||
teamPoints(wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
mostFalls(wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
mostTechs(wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
mostMajors(wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
mostDecisionPointsScored(wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
fastest_pins(wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
coinFlip(wrestlers_with_same_points) if ifWrestlersWithSamePointsIsSameAsOriginal(originalTieSize,wrestlers_with_same_points)
|
||||
end
|
||||
|
||||
|
||||
def headToHead
|
||||
wrestlersWithSamePoints.each do |wr|
|
||||
otherWrestler = wrestlersWithSamePoints.select{|w| w.id != wr.id}.first
|
||||
if otherWrestler and wr.match_against(otherWrestler).first.winner_id == wr.id
|
||||
addPointsToWrestlersAhead(wr)
|
||||
wr.pool_placement_tiebreaker = "Head to Head"
|
||||
def headToHead(wrestlers_with_same_points)
|
||||
wrestlers_with_same_points.each do |wr|
|
||||
otherWrestler = wrestlers_with_same_points.select{|w| w.id != wr.id}.first
|
||||
if otherWrestler
|
||||
matches = wr.match_against(otherWrestler).select { |match| match.bracket_position == "Pool" }
|
||||
if matches.any? && matches.first.winner == wr
|
||||
addPointsToWrestlersAhead(wr)
|
||||
wr.pool_placement_tiebreaker = "Head to Head"
|
||||
addPoints(wr)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def addPoints(wrestler)
|
||||
# addPointsToWrestlersAhead(wrestler)
|
||||
# Cannot go here because if team points are the same the first with points added will stay ahead
|
||||
wrestler.poolAdvancePoints = wrestler.poolAdvancePoints + 1
|
||||
end
|
||||
|
||||
@@ -87,13 +100,13 @@ class PoolOrder
|
||||
end
|
||||
end
|
||||
|
||||
def deductedPoints(originalTieSize)
|
||||
def deductedPoints(originalTieSize,wrestlers_with_same_points)
|
||||
pointsArray = []
|
||||
wrestlersWithSamePoints.each do |w|
|
||||
wrestlers_with_same_points.each do |w|
|
||||
pointsArray << w.total_points_deducted
|
||||
end
|
||||
leastPoints = pointsArray.min
|
||||
wrestlersWithLeastDeductedPoints = wrestlersWithSamePoints.select{|w| w.total_points_deducted == leastPoints}
|
||||
wrestlersWithLeastDeductedPoints = wrestlers_with_same_points.select{|w| w.total_points_deducted == leastPoints}
|
||||
addPointsToWrestlersAhead(wrestlersWithLeastDeductedPoints.first)
|
||||
if wrestlersWithLeastDeductedPoints.size != originalTieSize
|
||||
wrestlersWithLeastDeductedPoints.each do |wr|
|
||||
@@ -103,38 +116,37 @@ class PoolOrder
|
||||
end
|
||||
end
|
||||
|
||||
def mostDecisionPointsScored
|
||||
def mostDecisionPointsScored(wrestlers_with_same_points)
|
||||
pointsArray = []
|
||||
wrestlersWithSamePoints.each do |w|
|
||||
pointsArray << w.decision_points_scored
|
||||
wrestlers_with_same_points.each do |w|
|
||||
pointsArray << w.decision_points_scored_pool
|
||||
end
|
||||
mostPoints = pointsArray.max
|
||||
wrestlersWithMostPoints = wrestlersWithSamePoints.select{|w| w.decision_points_scored == mostPoints}
|
||||
wrestlersWithMostPoints = wrestlers_with_same_points.select{|w| w.decision_points_scored_pool == mostPoints}
|
||||
addPointsToWrestlersAhead(wrestlersWithMostPoints.first)
|
||||
wrestlersWithMostPoints.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Points Scored"
|
||||
wr.pool_placement_tiebreaker = "Decision Points Scored"
|
||||
addPoints(wr)
|
||||
end
|
||||
secondPoints = pointsArray.sort[-2]
|
||||
wrestlersWithSecondMostPoints = wrestlersWithSamePoints.select{|w| w.decision_points_scored == secondPoints}
|
||||
wrestlersWithSecondMostPoints = wrestlers_with_same_points.select{|w| w.decision_points_scored_pool == secondPoints}
|
||||
addPointsToWrestlersAhead(wrestlersWithSecondMostPoints.first)
|
||||
wrestlersWithSecondMostPoints.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Points Scored"
|
||||
wr.pool_placement_tiebreaker = "Decision Points Scored"
|
||||
addPoints(wr)
|
||||
end
|
||||
end
|
||||
|
||||
def fastest_pins
|
||||
def fastest_pins(wrestlers_with_same_points)
|
||||
wrestlersWithSamePointsWithPins = []
|
||||
wrestlersWithSamePoints.each do |wr|
|
||||
if wr.pin_wins.size > 0
|
||||
wrestlers_with_same_points.each do |wr|
|
||||
if wr.pin_wins.select{|m| m.bracket_position == "Pool"}.size > 0
|
||||
wrestlersWithSamePointsWithPins << wr
|
||||
end
|
||||
end
|
||||
if wrestlersWithSamePointsWithPins.size > 0
|
||||
fastest = wrestlersWithSamePointsWithPins.sort_by{|w| w.fastest_pin.pin_time_in_seconds}.first.fastest_pin
|
||||
secondFastest = wrestlersWithSamePointsWithPins.sort_by{|w| w.fastest_pin.pin_time_in_seconds}.second.fastest_pin
|
||||
wrestlersWithFastestPin = wrestlersWithSamePointsWithPins.select{|w| w.fastest_pin.pin_time_in_seconds == fastest.pin_time_in_seconds}
|
||||
fastest = wrestlersWithSamePointsWithPins.sort_by{|w| w.pin_time_pool}.first.pin_time_pool
|
||||
wrestlersWithFastestPin = wrestlersWithSamePointsWithPins.select{|w| w.pin_time_pool == fastest}
|
||||
addPointsToWrestlersAhead(wrestlersWithFastestPin.first)
|
||||
wrestlersWithFastestPin.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Pin Time"
|
||||
@@ -143,13 +155,13 @@ class PoolOrder
|
||||
end
|
||||
end
|
||||
|
||||
def teamPoints
|
||||
pointsArray = []
|
||||
wrestlersWithSamePoints.each do |w|
|
||||
pointsArray << w.total_pool_points_for_pool_order
|
||||
def teamPoints(wrestlers_with_same_points)
|
||||
teamPointsArray = []
|
||||
wrestlers_with_same_points.each do |w|
|
||||
teamPointsArray << w.total_pool_points_for_pool_order
|
||||
end
|
||||
mostPoints = pointsArray.max
|
||||
wrestlersSortedByTeamPoints = wrestlersWithSamePoints.select{|w| w.total_pool_points_for_pool_order == mostPoints}
|
||||
mostPoints = teamPointsArray.max
|
||||
wrestlersSortedByTeamPoints = wrestlers_with_same_points.select{|w| w.total_pool_points_for_pool_order == mostPoints}
|
||||
addPointsToWrestlersAhead(wrestlersSortedByTeamPoints.first)
|
||||
wrestlersSortedByTeamPoints.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Team Points"
|
||||
@@ -157,50 +169,56 @@ class PoolOrder
|
||||
end
|
||||
end
|
||||
|
||||
def mostFalls
|
||||
pointsArray = []
|
||||
wrestlersWithSamePoints.each do |w|
|
||||
pointsArray << w.pin_wins.size
|
||||
def mostFalls(wrestlers_with_same_points)
|
||||
mostPins = []
|
||||
wrestlers_with_same_points.each do |w|
|
||||
mostPins << w.pin_wins.select{|m| m.bracket_position == "Pool"}.size
|
||||
end
|
||||
pinsMax = mostPins.max
|
||||
wrestlersSortedByFallWins = wrestlers_with_same_points.select{|w| w.pin_wins.select{|m| m.bracket_position == "Pool"}.size == pinsMax}
|
||||
if pinsMax > 0
|
||||
addPointsToWrestlersAhead(wrestlersSortedByFallWins.first)
|
||||
wrestlersSortedByFallWins.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Most Pins"
|
||||
addPoints(wr)
|
||||
end
|
||||
end
|
||||
mostPoints = pointsArray.max
|
||||
wrestlersSortedByFallWins = wrestlersWithSamePoints.select{|w| w.pin_wins.size == mostPoints}
|
||||
addPointsToWrestlersAhead(wrestlersSortedByFallWins.first)
|
||||
wrestlersSortedByFallWins.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Most Pins"
|
||||
addPoints(wr)
|
||||
end
|
||||
end
|
||||
|
||||
def mostTechs
|
||||
pointsArray = []
|
||||
wrestlersWithSamePoints.each do |w|
|
||||
pointsArray << w.tech_wins.size
|
||||
def mostTechs(wrestlers_with_same_points)
|
||||
techsArray = []
|
||||
wrestlers_with_same_points.each do |w|
|
||||
techsArray << w.tech_wins.select{|m| m.bracket_position == "Pool"}.size
|
||||
end
|
||||
mostTechsWins = techsArray.max
|
||||
wrestlersSortedByTechWins = wrestlers_with_same_points.select{|w| w.tech_wins.select{|m| m.bracket_position == "Pool"}.size == mostTechsWins}
|
||||
if mostTechsWins > 0
|
||||
addPointsToWrestlersAhead(wrestlersSortedByTechWins.first)
|
||||
wrestlersSortedByTechWins.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Most Techs"
|
||||
addPoints(wr)
|
||||
end
|
||||
end
|
||||
mostPoints = pointsArray.max
|
||||
wrestlersSortedByTechWins = wrestlersWithSamePoints.select{|w| w.tech_wins.size == mostPoints}
|
||||
addPointsToWrestlersAhead(wrestlersSortedByTechWins.first)
|
||||
wrestlersSortedByTechWins.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Most Techs"
|
||||
addPoints(wr)
|
||||
end
|
||||
end
|
||||
|
||||
def mostMajors
|
||||
pointsArray = []
|
||||
wrestlersWithSamePoints.each do |w|
|
||||
pointsArray << w.major_wins.size
|
||||
def mostMajors(wrestlers_with_same_points)
|
||||
majorsArray = []
|
||||
wrestlers_with_same_points.each do |w|
|
||||
majorsArray << w.major_wins.select{|m| m.bracket_position == "Pool"}.size
|
||||
end
|
||||
mostMajorWins = majorsArray.max
|
||||
wrestlersSortedByMajorWins = wrestlers_with_same_points.select{|w| w.major_wins.select{|m| m.bracket_position == "Pool"}.size == mostMajorWins}
|
||||
if mostMajorWins > 0
|
||||
addPointsToWrestlersAhead(wrestlersSortedByMajorWins.first)
|
||||
wrestlersSortedByMajorWins.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Most Majors"
|
||||
addPoints(wr)
|
||||
end
|
||||
end
|
||||
mostPoints = pointsArray.max
|
||||
wrestlersSortedByMajorWins = wrestlersWithSamePoints.select{|w| w.major_wins.size == mostPoints}
|
||||
addPointsToWrestlersAhead(wrestlersSortedByMajorWins.first)
|
||||
wrestlersSortedByMajorWins.each do |wr|
|
||||
wr.pool_placement_tiebreaker = "Most Majors"
|
||||
addPoints(wr)
|
||||
end
|
||||
end
|
||||
|
||||
def coinFlip
|
||||
wrestler = wrestlersWithSamePoints.sample
|
||||
def coinFlip(wrestlers_with_same_points)
|
||||
wrestler = wrestlers_with_same_points.sample
|
||||
wrestler.pool_placement_tiebreaker = "Coin Flip"
|
||||
addPointsToWrestlersAhead(wrestler)
|
||||
addPoints(wrestler)
|
||||
|
||||
55
app/services/school_services/calculate_school_score.rb
Normal file
55
app/services/school_services/calculate_school_score.rb
Normal 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
|
||||
@@ -1,13 +1,186 @@
|
||||
class DoubleEliminationGenerateLoserNames
|
||||
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|
|
||||
next unless weight.calculate_bracket_size > 2
|
||||
|
||||
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)
|
||||
bracket_size = weight.calculate_bracket_size
|
||||
return if bracket_size <= 2
|
||||
|
||||
rows = match_rows.select { |row| row[:weight_id] == weight.id }
|
||||
num_placers = @tournament.number_of_placers
|
||||
|
||||
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
|
||||
|
||||
mappings = []
|
||||
champ_full[0...-1].each_with_index do |champ_info, i|
|
||||
map_idx = i.zero? ? 0 : (2 * i - 1)
|
||||
next if map_idx < 0 || map_idx >= conso_rounds.size
|
||||
|
||||
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
|
||||
}
|
||||
end
|
||||
|
||||
def assign_loser_names
|
||||
@tournament.weights.each do |weight|
|
||||
SixteenManDoubleEliminationGenerateLoserNames.new(weight).assign_loser_names_for_weight if weight.wrestlers.size >= 9 and weight.wrestlers.size <= 16
|
||||
EightManDoubleEliminationGenerateLoserNames.new(weight).assign_loser_names_for_weight if weight.wrestlers.size >= 4 and weight.wrestlers.size <= 8
|
||||
mappings.each do |map|
|
||||
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
|
||||
is_first_feed = map[:champ_round_index].zero?
|
||||
conso.each do |cm|
|
||||
champ_match1 = champ[idx]
|
||||
if champ_match1
|
||||
if is_first_feed && single_competitor_match_row?(champ_match1)
|
||||
cm[:loser1_name] = "BYE"
|
||||
else
|
||||
cm[:loser1_name] = "Loser of #{champ_match1[:bout_number]}"
|
||||
end
|
||||
else
|
||||
cm[:loser1_name] = nil
|
||||
end
|
||||
|
||||
if map[:both_wrestlers]
|
||||
idx += 1
|
||||
champ_match2 = champ[idx]
|
||||
if champ_match2
|
||||
if is_first_feed && single_competitor_match_row?(champ_match2)
|
||||
cm[:loser2_name] = "BYE"
|
||||
else
|
||||
cm[:loser2_name] = "Loser of #{champ_match2[:bout_number]}"
|
||||
end
|
||||
else
|
||||
cm[:loser2_name] = nil
|
||||
end
|
||||
end
|
||||
idx += 1
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def assign_bye_outcomes_in_memory(weight, match_rows)
|
||||
bracket_size = weight.calculate_bracket_size
|
||||
return if bracket_size <= 2
|
||||
|
||||
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) }
|
||||
|
||||
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
|
||||
|
||||
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}"
|
||||
end
|
||||
end
|
||||
|
||||
def dynamic_championship_rounds(size)
|
||||
total = Math.log2(size).to_i
|
||||
(1...total).map do |i|
|
||||
participants = size / (2**i)
|
||||
{ bracket_position: bracket_label(participants), round: i + 1 }
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
}
|
||||
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}"
|
||||
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
|
||||
"3/4"
|
||||
elsif participants == 4
|
||||
j.odd? ? "Conso Quarter" : "Conso Semis"
|
||||
else
|
||||
suffix = j.odd? ? ".1" : ".2"
|
||||
"Conso Round of #{participants}#{suffix}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,12 +1,250 @@
|
||||
class DoubleEliminationMatchGeneration
|
||||
def initialize( tournament )
|
||||
@tournament = tournament
|
||||
def initialize(tournament, weights: nil)
|
||||
@tournament = tournament
|
||||
@weights = weights
|
||||
end
|
||||
|
||||
def generate_matches
|
||||
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
|
||||
|
||||
def generate_matches
|
||||
@tournament.weights.each do |weight|
|
||||
SixteenManDoubleEliminationMatchGeneration.new(weight).generate_matches_for_weight if weight.wrestlers.size >= 9 and weight.wrestlers.size <= 16
|
||||
EightManDoubleEliminationMatchGeneration.new(weight).generate_matches_for_weight if weight.wrestlers.size >= 4 and weight.wrestlers.size <= 8
|
||||
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_match_rows_for_weight(weight)
|
||||
bracket_size = weight.calculate_bracket_size
|
||||
bracket_info = define_bracket_matches(bracket_size)
|
||||
return [] unless bracket_info
|
||||
|
||||
rows = []
|
||||
|
||||
# 1) Round one matchups
|
||||
bracket_info[:round_one_matchups].each_with_index do |matchup, idx|
|
||||
seed1, seed2 = matchup[:seeds]
|
||||
bracket_position = matchup[:bracket_position]
|
||||
bracket_pos_number = idx + 1
|
||||
round_number = matchup[:round]
|
||||
|
||||
rows << create_matchup_from_seed(
|
||||
seed1,
|
||||
seed2,
|
||||
bracket_position,
|
||||
bracket_pos_number,
|
||||
round_number,
|
||||
weight
|
||||
)
|
||||
end
|
||||
|
||||
# 2) Championship rounds
|
||||
bracket_info[:championship_rounds].each do |round_info|
|
||||
bracket_position = round_info[:bracket_position]
|
||||
matches_this_round = round_info[:number_of_matches]
|
||||
round_number = round_info[:round]
|
||||
|
||||
matches_this_round.times do |i|
|
||||
rows << create_matchup(
|
||||
nil,
|
||||
nil,
|
||||
bracket_position,
|
||||
i + 1,
|
||||
round_number,
|
||||
weight
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# 3) Consolation rounds
|
||||
bracket_info[:consolation_rounds].each do |round_info|
|
||||
bracket_position = round_info[:bracket_position]
|
||||
matches_this_round = round_info[:number_of_matches]
|
||||
round_number = round_info[:round]
|
||||
|
||||
matches_this_round.times do |i|
|
||||
rows << create_matchup(
|
||||
nil,
|
||||
nil,
|
||||
bracket_position,
|
||||
i + 1,
|
||||
round_number,
|
||||
weight
|
||||
)
|
||||
end
|
||||
|
||||
# 5/6, 7/8 placing logic
|
||||
if weight.wrestlers.size >= 5 && @tournament.number_of_placers >= 6 && matches_this_round == 1
|
||||
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
|
||||
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.
|
||||
# Returns a hash with :round_one_matchups, :championship_rounds, and :consolation_rounds.
|
||||
def define_bracket_matches(bracket_size)
|
||||
# Only support brackets that are powers of two
|
||||
return nil unless (bracket_size & (bracket_size - 1)).zero?
|
||||
|
||||
# 1) Generate the seed sequence (e.g., [1,8,5,4,...] for size=8)
|
||||
seeds = generate_seed_sequence(bracket_size)
|
||||
|
||||
# 2) Pair seeds into first-round matchups, sorting so lower seed is w1
|
||||
round_one = seeds.each_slice(2).map.with_index do |(s1, s2), idx|
|
||||
a, b = [s1, s2].sort
|
||||
{
|
||||
seeds: [a, b],
|
||||
bracket_position: first_round_label(bracket_size),
|
||||
round: 1
|
||||
}
|
||||
end
|
||||
|
||||
# 3) Build full structure, including dynamic championship & consolation rounds
|
||||
{
|
||||
round_one_matchups: round_one,
|
||||
championship_rounds: dynamic_championship_rounds(bracket_size),
|
||||
consolation_rounds: dynamic_consolation_rounds(bracket_size)
|
||||
}
|
||||
end
|
||||
|
||||
# Returns a human-readable label for the first round based on bracket size.
|
||||
def first_round_label(bracket_size)
|
||||
case bracket_size
|
||||
when 2 then "1/2"
|
||||
when 4 then "Semis"
|
||||
when 8 then "Quarter"
|
||||
else "Bracket Round of #{bracket_size}"
|
||||
end
|
||||
end
|
||||
|
||||
# Dynamically generate championship rounds for any power-of-two bracket size.
|
||||
def dynamic_championship_rounds(bracket_size)
|
||||
rounds = []
|
||||
num_rounds = Math.log2(bracket_size).to_i
|
||||
# i: 1 -> first post-initial round, up to num_rounds-1 (final)
|
||||
(1...num_rounds).each do |i|
|
||||
participants = bracket_size / (2**i)
|
||||
number_of_matches = participants / 2
|
||||
bracket_position = case participants
|
||||
when 2 then "1/2"
|
||||
when 4 then "Semis"
|
||||
when 8 then "Quarter"
|
||||
else "Bracket Round of #{participants}"
|
||||
end
|
||||
round_number = i * 2
|
||||
rounds << { bracket_position: bracket_position,
|
||||
number_of_matches: number_of_matches,
|
||||
round: round_number }
|
||||
end
|
||||
rounds
|
||||
end
|
||||
|
||||
# Dynamically generate consolation rounds for any power-of-two bracket size.
|
||||
def dynamic_consolation_rounds(bracket_size)
|
||||
rounds = []
|
||||
num_rounds = Math.log2(bracket_size).to_i
|
||||
total_conso = 2 * (num_rounds - 1) - 1
|
||||
(1..total_conso).each do |j|
|
||||
participants = bracket_size / (2**((j.to_f / 2).ceil))
|
||||
number_of_matches = participants / 2
|
||||
bracket_position = case participants
|
||||
when 2 then "3/4"
|
||||
when 4
|
||||
j.odd? ? "Conso Quarter" : "Conso Semis"
|
||||
else
|
||||
suffix = j.odd? ? ".1" : ".2"
|
||||
"Conso Round of #{participants}#{suffix}"
|
||||
end
|
||||
round_number = j + 1
|
||||
rounds << { bracket_position: bracket_position,
|
||||
number_of_matches: number_of_matches,
|
||||
round: round_number }
|
||||
end
|
||||
rounds
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
# PHASE 2: Overwrite rounds in all smaller brackets to match the largest one.
|
||||
###########################################################################
|
||||
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 = {}
|
||||
rows_by_weight_id.fetch(largest_weight.id, []).each do |row|
|
||||
position_to_round[row[:bracket_position]] ||= row[:round]
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
###########################################################################
|
||||
# Helper methods
|
||||
###########################################################################
|
||||
def generation_weights
|
||||
@weights || @tournament.weights.to_a
|
||||
end
|
||||
|
||||
def wrestler_with_seed(seed, weight)
|
||||
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)
|
||||
create_matchup(
|
||||
wrestler_with_seed(w1_seed, weight),
|
||||
wrestler_with_seed(w2_seed, weight),
|
||||
bracket_position,
|
||||
bracket_position_number,
|
||||
round,
|
||||
weight
|
||||
)
|
||||
end
|
||||
|
||||
def create_matchup(w1, w2, bracket_position, bracket_position_number, round, weight)
|
||||
{
|
||||
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.
|
||||
def generate_seed_sequence(n)
|
||||
raise ArgumentError, "Bracket size must be a power of two" unless (n & (n - 1)).zero?
|
||||
return [1, 2] if n == 2
|
||||
|
||||
half = n / 2
|
||||
prev = generate_seed_sequence(half)
|
||||
comp = prev.map { |s| n + 1 - s }
|
||||
|
||||
result = []
|
||||
(0...prev.size).step(2) do |k|
|
||||
result << prev[k]
|
||||
result << comp[k]
|
||||
result << comp[k + 1]
|
||||
result << prev[k + 1]
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
class EightManDoubleEliminationGenerateLoserNames
|
||||
def initialize( weight )
|
||||
@weight = weight
|
||||
end
|
||||
|
||||
def assign_loser_names_for_weight
|
||||
matches_by_weight = nil
|
||||
matches_by_weight = @weight.matches
|
||||
conso_round_2(matches_by_weight)
|
||||
conso_round_4(matches_by_weight)
|
||||
fifth_sixth(matches_by_weight)
|
||||
save_matches(matches_by_weight)
|
||||
matches_by_weight = @weight.matches.reload
|
||||
advance_bye_matches_championship(matches_by_weight)
|
||||
save_matches(matches_by_weight)
|
||||
end
|
||||
|
||||
def conso_round_2(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 == 1 and m.round == 1 and m.bracket_position == "Quarter"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.round == 1 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.round == 1 and m.bracket_position == "Quarter"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.round == 1 and m.bracket_position == "Quarter"}.first.bout_number}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def conso_round_4(matches)
|
||||
matches.select{|m| m.bracket_position == "Conso Semis"}.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 == 2 and m.bracket_position == "Semis"}.first.bout_number}"
|
||||
elsif match.bracket_position_number == 2
|
||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 1 and m.bracket_position == "Semis"}.first.bout_number}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fifth_sixth(matches)
|
||||
matches.select{|m| m.bracket_position == "5/6"}.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 advance_bye_matches_championship(matches)
|
||||
matches.select{|m| m.round == 1 and m.bracket_position == "Quarter"}.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.save
|
||||
match.advance_wrestlers
|
||||
elsif match.w2 != nil
|
||||
match.winner_id = match.w2
|
||||
match.loser1_name = "BYE"
|
||||
match.save
|
||||
match.advance_wrestlers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def save_matches(matches)
|
||||
matches.each do |m|
|
||||
m.save!
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,63 +0,0 @@
|
||||
class EightManDoubleEliminationMatchGeneration
|
||||
def initialize( weight )
|
||||
@weight = weight
|
||||
end
|
||||
|
||||
def generate_matches_for_weight
|
||||
round_one(@weight)
|
||||
round_two(@weight)
|
||||
round_three(@weight)
|
||||
round_four(@weight)
|
||||
end
|
||||
|
||||
def round_one(weight)
|
||||
create_matchup_from_seed(1,8,"Quarter",1,1,weight)
|
||||
create_matchup_from_seed(4,5,"Quarter",2,1,weight)
|
||||
create_matchup_from_seed(3,6,"Quarter",3,1,weight)
|
||||
create_matchup_from_seed(2,7,"Quarter",4,1,weight)
|
||||
end
|
||||
|
||||
def round_two(weight)
|
||||
create_matchup(nil,nil,"Semis",1,2,weight)
|
||||
create_matchup(nil,nil,"Semis",2,2,weight)
|
||||
create_matchup(nil,nil,"Conso Quarter",1,2,weight)
|
||||
create_matchup(nil,nil,"Conso Quarter",2,2,weight)
|
||||
end
|
||||
|
||||
def round_three(weight)
|
||||
create_matchup(nil,nil,"Conso Semis",1,3,weight)
|
||||
create_matchup(nil,nil,"Conso Semis",2,3,weight)
|
||||
end
|
||||
|
||||
def round_four(weight)
|
||||
create_matchup(nil,nil,"1/2",1,4,weight)
|
||||
create_matchup(nil,nil,"3/4",1,4,weight)
|
||||
create_matchup(nil,nil,"5/6",1,4,weight)
|
||||
end
|
||||
|
||||
def wrestler_with_seed(seed,weight)
|
||||
wrestler = Wrestler.where("weight_id = ? and bracket_line = ?", weight.id, seed).first
|
||||
if wrestler
|
||||
return wrestler.id
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def create_matchup_from_seed(w1_seed, w2_seed, bracket_position, bracket_position_number,round,weight)
|
||||
# if wrestler_with_seed(w1_seed,weight) and wrestler_with_seed(w2_seed,weight)
|
||||
create_matchup(wrestler_with_seed(w1_seed,weight),wrestler_with_seed(w2_seed,weight), bracket_position, bracket_position_number,round,weight)
|
||||
# end
|
||||
end
|
||||
|
||||
def create_matchup(w1, w2, bracket_position, bracket_position_number,round,weight)
|
||||
@weight.tournament.matches.create(
|
||||
w1: w1,
|
||||
w2: w2,
|
||||
weight_id: weight.id,
|
||||
round: round,
|
||||
bracket_position: bracket_position,
|
||||
bracket_position_number: bracket_position_number
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -3,102 +3,190 @@ class GenerateTournamentMatches
|
||||
@tournament = tournament
|
||||
end
|
||||
|
||||
def generateWeight(weight)
|
||||
if Rails.env.production?
|
||||
self.delay(:job_owner_id => @tournament.id, :job_owner_type => "Generate matches for weights class #{weight.max}").generate_weight_raw(weight)
|
||||
else
|
||||
self.generate_weight_raw(weight)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_weight_raw(weight)
|
||||
WipeTournamentMatches.new(@tournament).wipeWeightMatches(weight)
|
||||
@tournament.curently_generating_matches = 1
|
||||
@tournament.save
|
||||
unAssignBouts
|
||||
unAssignMats
|
||||
PoolToBracketMatchGeneration.new(@tournament).generatePoolToBracketMatchesWeight(weight) if @tournament.tournament_type == "Pool to bracket"
|
||||
postMatchCreationActions
|
||||
PoolToBracketGenerateLoserNames.new(@tournament).assignLoserNamesWeight(weight) if @tournament.tournament_type == "Pool to bracket"
|
||||
end
|
||||
|
||||
def generate
|
||||
if Rails.env.production?
|
||||
self.delay(:job_owner_id => @tournament.id, :job_owner_type => "Generate matches for all weights").generate_raw
|
||||
else
|
||||
self.generate_raw
|
||||
end
|
||||
# Use perform_later which will execute based on centralized adapter config
|
||||
GenerateTournamentMatchesJob.perform_later(@tournament)
|
||||
end
|
||||
|
||||
def generate_raw
|
||||
standardStartingActions
|
||||
PoolToBracketMatchGeneration.new(@tournament).generatePoolToBracketMatches if @tournament.tournament_type == "Pool to bracket"
|
||||
ModifiedSixteenManMatchGeneration.new(@tournament).generate_matches if @tournament.tournament_type == "Modified 16 Man Double Elimination"
|
||||
DoubleEliminationMatchGeneration.new(@tournament).generate_matches if @tournament.tournament_type == "Double Elimination 1-6"
|
||||
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 == "Modified 16 Man Double Elimination"
|
||||
DoubleEliminationGenerateLoserNames.new(@tournament).assign_loser_names if @tournament.tournament_type == "Double Elimination 1-6"
|
||||
advance_bye_matches_after_insert
|
||||
end
|
||||
|
||||
def standardStartingActions
|
||||
@tournament.curently_generating_matches = 1
|
||||
@tournament.save
|
||||
WipeTournamentMatches.new(@tournament).setUpMatchGeneration
|
||||
TournamentSeeding.new(@tournament).setSeeds
|
||||
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
|
||||
assignBouts
|
||||
assignFirstMatchesToMats
|
||||
@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
|
||||
end
|
||||
|
||||
def assignFirstMatchesToMats
|
||||
matsToAssign = @tournament.mats
|
||||
if matsToAssign.count > 0
|
||||
until matsToAssign.sort_by{|m| m.id}.last.matches.count == 4
|
||||
matsToAssign.sort_by{|m| m.id}.each do |m|
|
||||
m.assign_next_match
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
@@ -1,86 +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)
|
||||
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.round == 2 and m.bracket_position == "Conso"}.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.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
elsif match.bracket_position_number == 2
|
||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 3 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
elsif match.bracket_position_number == 3
|
||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 5 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 6 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
elsif match.bracket_position_number == 4
|
||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 7 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 8 and m.round == 1 and m.bracket_position == "Bracket"}.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.round == 3 and 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 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.round == 1 and m.bracket_position == "Bracket"}.sort_by{|m| m.bracket_position_number}.each do |match|
|
||||
if match.w1 == nil or match.w2 == nil
|
||||
puts match.bout_number
|
||||
match.finished = 1
|
||||
match.win_type = "BYE"
|
||||
if match.w1 != nil
|
||||
match.winner_id = match.w1
|
||||
match.loser2_name = "BYE"
|
||||
match.save
|
||||
match.advance_wrestlers
|
||||
elsif match.w2 != nil
|
||||
match.winner_id = match.w2
|
||||
match.loser1_name = "BYE"
|
||||
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
|
||||
|
||||
@@ -1,66 +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_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, 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, 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, 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, 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, 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
|
||||
rows << create_matchup(nil,nil,"7/8",1,5,weight)
|
||||
end
|
||||
end
|
||||
|
||||
def generate_matches_for_weight(weight)
|
||||
round_one(weight)
|
||||
round_two(weight)
|
||||
round_three(weight)
|
||||
round_four(weight)
|
||||
round_five(weight)
|
||||
end
|
||||
|
||||
def round_one(weight)
|
||||
create_matchup_from_seed(1,16, "Bracket", 1, 1,weight)
|
||||
create_matchup_from_seed(8,9, "Bracket", 2, 1,weight)
|
||||
create_matchup_from_seed(5,12, "Bracket", 3, 1,weight)
|
||||
create_matchup_from_seed(4,14, "Bracket", 4, 1,weight)
|
||||
create_matchup_from_seed(3,13, "Bracket", 5, 1,weight)
|
||||
create_matchup_from_seed(6,11, "Bracket", 6, 1,weight)
|
||||
create_matchup_from_seed(7,10, "Bracket", 7, 1,weight)
|
||||
create_matchup_from_seed(2,15, "Bracket", 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",1,2,weight)
|
||||
create_matchup(nil,nil,"Conso",2,2,weight)
|
||||
create_matchup(nil,nil,"Conso",3,2,weight)
|
||||
create_matchup(nil,nil,"Conso",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)
|
||||
end
|
||||
|
||||
def round_four(weight)
|
||||
create_matchup(nil,nil,"Conso Semis",1,4,weight)
|
||||
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)
|
||||
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
|
||||
@@ -75,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,52 +1,92 @@
|
||||
class PoolToBracketMatchGeneration
|
||||
def initialize( tournament )
|
||||
def initialize(tournament, weights: nil, wrestlers_by_weight_id: nil)
|
||||
@tournament = tournament
|
||||
end
|
||||
|
||||
def generatePoolToBracketMatchesWeight(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()
|
||||
setOriginalSeedsToWrestleLastPoolRound(weight)
|
||||
@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
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
class SixteenManDoubleEliminationGenerateLoserNames
|
||||
def initialize( weight )
|
||||
@weight = weight
|
||||
end
|
||||
|
||||
def assign_loser_names_for_weight
|
||||
matches_by_weight = nil
|
||||
matches_by_weight = @weight.matches
|
||||
conso_round_2(matches_by_weight)
|
||||
conso_round_3(matches_by_weight)
|
||||
conso_round_5(matches_by_weight)
|
||||
fifth_sixth(matches_by_weight)
|
||||
save_matches(matches_by_weight)
|
||||
matches_by_weight = @weight.matches.reload
|
||||
advance_bye_matches_championship(matches_by_weight)
|
||||
save_matches(matches_by_weight)
|
||||
end
|
||||
|
||||
def conso_round_2(matches)
|
||||
matches.select{|m| m.round == 2 and m.bracket_position == "Conso"}.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.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
elsif match.bracket_position_number == 2
|
||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 3 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
elsif match.bracket_position_number == 3
|
||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 5 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 6 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
elsif match.bracket_position_number == 4
|
||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 7 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 8 and m.round == 1 and m.bracket_position == "Bracket"}.first.bout_number}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def conso_round_3(matches)
|
||||
matches.select{|m| m.round == 3 and m.bracket_position == "Conso"}.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
|
||||
|
||||
def conso_round_5(matches)
|
||||
matches.select{|m| m.round == 5 and m.bracket_position == "Conso Semis"}.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 == "Semis"}.first.bout_number}"
|
||||
elsif match.bracket_position_number == 2
|
||||
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.bracket_position == "Semis"}.first.bout_number}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fifth_sixth(matches)
|
||||
matches.select{|m| m.bracket_position == "5/6"}.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 advance_bye_matches_championship(matches)
|
||||
matches.select{|m| m.round == 1 and m.bracket_position == "Bracket"}.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.save
|
||||
match.advance_wrestlers
|
||||
elsif match.w2 != nil
|
||||
match.winner_id = match.w2
|
||||
match.loser1_name = "BYE"
|
||||
match.save
|
||||
match.advance_wrestlers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def save_matches(matches)
|
||||
matches.each do |m|
|
||||
m.save!
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,87 +0,0 @@
|
||||
class SixteenManDoubleEliminationMatchGeneration
|
||||
def initialize( weight )
|
||||
@weight = weight
|
||||
end
|
||||
|
||||
def generate_matches_for_weight
|
||||
round_one(@weight)
|
||||
round_two(@weight)
|
||||
round_three(@weight)
|
||||
round_four(@weight)
|
||||
round_five(@weight)
|
||||
round_six(@weight)
|
||||
end
|
||||
|
||||
def round_one(weight)
|
||||
create_matchup_from_seed(1,16, "Bracket", 1, 1,weight)
|
||||
create_matchup_from_seed(8,9, "Bracket", 2, 1,weight)
|
||||
create_matchup_from_seed(5,12, "Bracket", 3, 1,weight)
|
||||
create_matchup_from_seed(4,14, "Bracket", 4, 1,weight)
|
||||
create_matchup_from_seed(3,13, "Bracket", 5, 1,weight)
|
||||
create_matchup_from_seed(6,11, "Bracket", 6, 1,weight)
|
||||
create_matchup_from_seed(7,10, "Bracket", 7, 1,weight)
|
||||
create_matchup_from_seed(2,15, "Bracket", 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",1,2,weight)
|
||||
create_matchup(nil,nil,"Conso",2,2,weight)
|
||||
create_matchup(nil,nil,"Conso",3,2,weight)
|
||||
create_matchup(nil,nil,"Conso",4,2,weight)
|
||||
end
|
||||
|
||||
def round_three(weight)
|
||||
create_matchup(nil,nil,"Conso",1,3,weight)
|
||||
create_matchup(nil,nil,"Conso",2,3,weight)
|
||||
create_matchup(nil,nil,"Conso",3,3,weight)
|
||||
create_matchup(nil,nil,"Conso",4,3,weight)
|
||||
end
|
||||
|
||||
def round_four(weight)
|
||||
create_matchup(nil,nil,"Semis",1,4,weight)
|
||||
create_matchup(nil,nil,"Semis",2,4,weight)
|
||||
create_matchup(nil,nil,"Conso Quarter",1,4,weight)
|
||||
create_matchup(nil,nil,"Conso Quarter",2,4,weight)
|
||||
end
|
||||
|
||||
def round_five(weight)
|
||||
create_matchup(nil,nil,"Conso Semis",1,5,weight)
|
||||
create_matchup(nil,nil,"Conso Semis",2,5,weight)
|
||||
end
|
||||
|
||||
def round_six(weight)
|
||||
create_matchup(nil,nil,"1/2",1,6,weight)
|
||||
create_matchup(nil,nil,"3/4",1,6,weight)
|
||||
create_matchup(nil,nil,"5/6",1,6,weight)
|
||||
end
|
||||
|
||||
def wrestler_with_seed(seed,weight)
|
||||
wrestler = Wrestler.where("weight_id = ? and bracket_line = ?", weight.id, seed).first
|
||||
if wrestler
|
||||
return wrestler.id
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def create_matchup_from_seed(w1_seed, w2_seed, bracket_position, bracket_position_number,round,weight)
|
||||
# if wrestler_with_seed(w1_seed,weight) and wrestler_with_seed(w2_seed,weight)
|
||||
create_matchup(wrestler_with_seed(w1_seed,weight),wrestler_with_seed(w2_seed,weight), bracket_position, bracket_position_number,round,weight)
|
||||
# end
|
||||
end
|
||||
|
||||
def create_matchup(w1, w2, bracket_position, bracket_position_number,round,weight)
|
||||
@weight.tournament.matches.create(
|
||||
w1: w1,
|
||||
w2: w2,
|
||||
weight_id: weight.id,
|
||||
round: round,
|
||||
bracket_position: bracket_position,
|
||||
bracket_position_number: bracket_position_number
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
class TournamentBackupService
|
||||
def initialize(tournament, reason)
|
||||
@tournament = tournament
|
||||
@reason = reason
|
||||
end
|
||||
|
||||
def create_backup
|
||||
# Use perform_later which will execute based on centralized adapter config
|
||||
TournamentBackupJob.perform_later(@tournament, @reason)
|
||||
end
|
||||
|
||||
def create_backup_raw
|
||||
# Generate the JSON directly in Ruby and encode it
|
||||
backup_data = Base64.encode64(generate_json.to_json)
|
||||
|
||||
begin
|
||||
# Save the backup with encoded data
|
||||
TournamentBackup.create!(tournament: @tournament, backup_data: backup_data, backup_reason: @reason)
|
||||
Rails.logger.info("Backup created successfully for tournament ##{@tournament.id}")
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
Rails.logger.error("Failed to save backup: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_json
|
||||
@tournament.reload
|
||||
@tournament.schools.reload
|
||||
@tournament.weights.reload
|
||||
@tournament.mats.reload
|
||||
@tournament.mat_assignment_rules.reload
|
||||
@tournament.wrestlers.reload
|
||||
@tournament.matches.reload
|
||||
data = {
|
||||
tournament: {
|
||||
attributes: @tournament.attributes,
|
||||
schools: @tournament.schools.map(&:attributes),
|
||||
weights: @tournament.weights.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"),
|
||||
# Emit the human-readable max values under a distinct key to avoid
|
||||
# colliding with the raw DB-backed "weight_classes" attribute (which
|
||||
# is stored as a comma-separated string). Using a different key
|
||||
# prevents duplicate JSON keys when symbols and strings are both present.
|
||||
"weight_class_maxes" => rule.weight_classes.map do |weight_id|
|
||||
Weight.find_by(id: weight_id)&.max
|
||||
end
|
||||
)
|
||||
end,
|
||||
wrestlers: @tournament.wrestlers.map do |wrestler|
|
||||
wrestler.attributes.merge(
|
||||
school: wrestler.school&.attributes,
|
||||
weight: wrestler.weight&.attributes
|
||||
)
|
||||
end,
|
||||
matches: @tournament.matches.sort_by(&:bout_number).map do |match|
|
||||
match.attributes.merge(
|
||||
w1_name: match.wrestler1&.name,
|
||||
w2_name: match.wrestler2&.name,
|
||||
winner_name: match.winner&.name,
|
||||
weight: match.weight&.attributes,
|
||||
mat: match.mat&.attributes
|
||||
)
|
||||
end
|
||||
}
|
||||
}
|
||||
# puts "Generated JSON for backup: #{data[:tournament][:mats]}"
|
||||
data
|
||||
end
|
||||
end
|
||||
@@ -3,42 +3,118 @@ class TournamentSeeding
|
||||
@tournament = tournament
|
||||
end
|
||||
|
||||
def setSeeds
|
||||
@tournament.weights.each do |w|
|
||||
resetAllSeeds(w)
|
||||
setOriginalSeeds(w)
|
||||
randomSeeding(w)
|
||||
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)
|
||||
updated_wrestlers.concat(wrestlers)
|
||||
end
|
||||
|
||||
persist_bracket_lines(updated_wrestlers) if persist
|
||||
updated_wrestlers
|
||||
end
|
||||
|
||||
def randomSeeding(weight)
|
||||
wrestlerWithSeeds = weight.wrestlers.select{|w| w.original_seed != nil }.sort_by{|w| w.original_seed}
|
||||
if wrestlerWithSeeds.size > 0
|
||||
highestSeed = wrestlerWithSeeds.last.bracket_line
|
||||
seed = highestSeed + 1
|
||||
def random_seeding(wrestlers, bracket_size)
|
||||
half_of_bracket = bracket_size / 2
|
||||
available_bracket_lines = (1..bracket_size).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)
|
||||
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|
|
||||
# pool brackets just grab the first available seed
|
||||
first_available_bracket_line = available_bracket_lines.first
|
||||
wrestler.bracket_line = first_available_bracket_line
|
||||
available_bracket_lines.delete(first_available_bracket_line)
|
||||
end
|
||||
else
|
||||
seed = 1
|
||||
end
|
||||
wrestlersWithoutSeed = weight.wrestlers.select{|w| w.original_seed == nil }
|
||||
wrestlersWithoutSeed.shuffle.each do |w|
|
||||
w.bracket_line = seed
|
||||
w.save
|
||||
seed += 1
|
||||
# Iterrate over the list randomly
|
||||
wrestlers_without_bracket_lines.shuffle.each do |wrestler|
|
||||
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
|
||||
return wrestlers
|
||||
end
|
||||
|
||||
def setOriginalSeeds(weight)
|
||||
wrestlerWithSeeds = weight.wrestlers.select{|w| w.original_seed != nil }
|
||||
wrestlerWithSeeds.each do |w|
|
||||
w.bracket_line = w.original_seed
|
||||
w.save
|
||||
def set_original_seed_to_bracket_line(wrestlers)
|
||||
wrestlers_with_seeds = wrestlers.select{|w| w.original_seed != nil }
|
||||
wrestlers_with_seeds.each do |wrestler|
|
||||
wrestlers_with_seeded_wrestlers_bracket_line = wrestlers.select{|w| w.bracket_line == wrestler.original_seed && w.id != wrestler.id}
|
||||
wrestlers_with_seeded_wrestlers_bracket_line.each do |wrestler_with_wrong_bracket_line|
|
||||
wrestler_with_wrong_bracket_line.bracket_line = nil
|
||||
end
|
||||
|
||||
wrestler.bracket_line = wrestler.original_seed
|
||||
end
|
||||
return wrestlers
|
||||
end
|
||||
|
||||
def resetAllSeeds(weight)
|
||||
weight.wrestlers.each do |w|
|
||||
def reset_bracket_line_for_lines_higher_than_bracket_size(wrestlers, bracket_size)
|
||||
wrestlers.each do |w|
|
||||
if w.bracket_line && w.bracket_line > bracket_size
|
||||
w.bracket_line = nil
|
||||
end
|
||||
end
|
||||
return wrestlers
|
||||
end
|
||||
|
||||
def reset_all_seeds(wrestlers)
|
||||
wrestlers.each do |w|
|
||||
w.bracket_line = nil
|
||||
w.save
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user