1
0
mirror of https://github.com/jcwimer/wrestlingApp synced 2026-03-25 01:14:43 +00:00

197 Commits

Author SHA1 Message Date
af2fc3feba Fixed a test after changing links to turbo 2025-11-11 21:55:36 -05:00
793a9e3ecc All links with a confirm now use turbo 2025-11-11 21:09:24 -05:00
f73e9bfc4e Fix the reset bout board link 2025-11-11 20:55:34 -05:00
92bd06fe3c No longer using memcached. Replication settings for standalong mariadb. Use --single-transaction in mariadb replica-watcher so mysqldump does not lock tables. Added horizontal pod autoscaler to the app statefulset 2025-10-30 08:50:31 -04:00
6e9554be55 Fixed the JSON 3 deprecation in the backup and import service 2025-10-08 13:54:38 -04:00
34f1783031 Upgraded to rails 8.0.2 2025-10-08 11:35:44 -04:00
bbd2bd9b44 Fixed ads.txt 2025-10-07 15:31:04 -04:00
6ecebba70d Updated gems 2025-10-07 15:30:47 -04:00
e64751e471 Fixed name of db to replicate 2025-10-03 08:50:34 -04:00
d0f19e855f Added a mariadb replica watcher to fix replication issues 2025-09-30 16:31:43 -04:00
3e1ae22b6b Added pagination for the tournaments index page 2025-09-26 12:31:37 -04:00
15f85e439c Fixed finish_tournament_204.rake 2025-09-15 18:57:29 -04:00
c5b9783853 Fixed logout link 2025-09-15 18:32:55 -04:00
cd77268070 Trying to make the finish_tournament_204 rake job reliable 2025-09-15 18:28:35 -04:00
d61ed80287 Reload last_match and wrestler in advanced_wrestler. Moved calculate team score to the end of advance_wrestler. 2025-09-15 18:22:42 -04:00
dd5ce9bd60 Fixed my ads.txt to contain my publisher id for adsense 2025-09-15 17:31:04 -04:00
9a4e6f6597 Dynamic double elim match generation and loser name generation 2025-09-02 22:10:55 -04:00
782baedcfe Added support for 64 man brackets 2025-08-28 10:32:35 -04:00
Your Name
53e16952bf Added Stimulus and moved the matstats vanilla js to stimulus controllers. Same with the spectate page. 2025-05-20 17:22:48 -04:00
Your Name
0326d87261 Migrated from Sprockets to Propshaft. 2025-05-16 17:14:05 -04:00
5296b71bb9 Downloaded fontawesome locally instead of using CDN 2025-05-15 17:09:24 -04:00
58be6b8074 gitignore .DS_Store and update Gemfile 2025-05-15 13:58:02 -04:00
4accedbb43 Added a daily recurring job to cleanup tournaments. Fixed final score fields not loading without page refresh on mat stats page and added a cypress test for it. 2025-05-07 16:01:48 -04:00
2856060b11 Added cypress tests for mat stats javascript. 2025-05-05 19:57:03 -04:00
68a7b214c9 Fixed match stats when localstorage is empty but server data is not. 2025-04-28 16:35:24 -04:00
1fcaec876f Refactored match winner and wrestlers matches and fixed slow tests. 2025-04-25 15:59:35 -04:00
3e4317dbc5 Using action cable to send stats updates to the client and made a spectate page. 2025-04-21 17:12:54 -04:00
44fb5388b4 Made the all results page grouping better and fixed the advance wrestler job. 2025-04-17 17:34:34 -04:00
ed7186e5ce Moved the tournament navbar to the bottom of the page and made site responsive. Fixed puma solid queue in development. Added a note about clobbering assets in the README. Fixed the ad blocker check due to turbolinks it had to be idempotent. Added migrations for all dbs in the rails-dev-db-create.sh script. 2025-04-16 16:19:29 -04:00
6e61a7245a Added a separate table to record background job status for tournaments and fixed migrations/schemas for solid dbs. Foreign key constraints are now added to the migrations where we do belongs_to. 2025-04-15 16:16:44 -04:00
4828d9b876 Fixed reset password after the Devise migration. 2025-04-11 07:59:55 -04:00
2d433b680a Upgraded to rails 8.0.2, moved from dalli to solid cache, moved from delayed_job to solid queue, and add solid cable. deploy/rails-dev-run.sh no longer needs to chmod. Fixed finished_at callback for matches. Migrated from Devise to built in rails auth. Added view tests for the bracket page testing that all bout numbers render for all matches in each bracket type. 2025-04-08 17:54:42 -04:00
9c25a6cc39 New bracket positions for double elim brackets. Each bracket position will now wrestle during the same round. Made a rake task to migrate previous matches to the new bracket positions. 2025-04-02 16:23:20 -04:00
f32e711d2b Updated gems 2025-03-11 12:37:49 -04:00
010d9a5f6b Added all_results page to tournaments 2025-03-11 12:32:18 -04:00
91e1939e69 Added a feature to generate uuid links for coaches to submit their school lineups. 2025-02-18 16:39:10 -05:00
934b34d0b7 Properly place wrestlers in the correct pool dynamically 2025-02-10 13:26:25 -05:00
63b91f7ddd Added rvm info to the README 2025-01-27 09:10:23 -05:00
dd0585ed55 Fixed bout sheets to fit on a single page. 2025-01-27 09:10:02 -05:00
735f7090b2 For double elimm printable brackets, only spill onto second page for brackets over 16 wrestlers. 2025-01-25 20:43:52 -05:00
690e497654 Fixed mat assignment rules to be db agnostic with comma delimited strings and upgraded test env db to mariadb 10.10 to match production. 2025-01-25 20:02:22 -05:00
54655a2ea9 Fixed the print view for brackets 2025-01-24 18:04:02 -05:00
5114ed7b08 Added a finished_at column on matches, created a callback to updated that only if finished changes and is 1, and display that on the match stats page. 2025-01-22 17:01:06 -05:00
f6bc7aa1a4 Fixed matchstats error where putting in a score then changing win type to pin does not unlock the submit button. 2025-01-22 09:21:45 -05:00
3248fdf7ca Fixed dynamic form to correctly update the final score when submitting the form 2025-01-21 16:14:15 -05:00
60814fdd94 Added a next bout number to the mat show page on the match stats 2025-01-20 11:41:07 -05:00
cc62e1c2f1 Protect tournament from out of order seeding. 2025-01-20 10:23:27 -05:00
d57b2e6e6f Make seeds truly random for double elimination brackets 2025-01-19 22:32:28 -05:00
fd1a7c43ff Fix all brackets printable view. 2025-01-19 21:58:02 -05:00
b241818c21 Added underscores in the stats box for the other wrestler to make it easier to follow. 2025-01-19 20:57:46 -05:00
288cb6704e Added a dynamic score form on the stats page, added stats page form validation on the in js to compliment the validation already in place on the model, and added the wrestler's score in the winner dropdown of the form. 2025-01-19 20:35:33 -05:00
c45ec8ab38 Fixed mat assignment rules. MySQL required implicit conversion to an integer. 2025-01-12 08:15:45 -05:00
e612a4b10e Fixed the query for unassigned matches on the up_matches page and also only allow mats to get a new match assigned if they have less than 4 unfinished matches. Added mats to the finish_tournament_204 rake task. 2025-01-10 16:02:37 -05:00
38785924ed Fixed merge conflict for tournament backups migration 2025-01-08 08:41:42 -05:00
275847befe Added a tournament error if more than 1 person in a weight has the same seed. 2025-01-07 16:00:17 -05:00
b5150a3a37 Added a rake task that randomly selects winners for tournament 204 from seeded tournaments. 2025-01-07 08:19:21 -05:00
6adcc709e7 Do not show matches with a BYE on the upcoming matches on the bout board. Those matches are filtered out on the next available match logic for the mats. 2025-01-07 08:18:45 -05:00
bb3b05ee81 Fixed auto byes and wrote a test 2025-01-05 10:32:16 -05:00
490bd762f6 Fixed seeding to go in order to make sure there are never double byes in the first round 2025-01-04 20:26:51 -05:00
210e763d4c Increase the delayed job handler column size to match the tournament backups backup data size. This was preventing import jobs from saving. 2025-01-04 19:19:08 -05:00
f3e0f5d4c5 Added integration tests for import and backup 2025-01-04 18:14:33 -05:00
50055fc278 Fixed a typo in the 32 man run through test 2025-01-04 16:31:18 -05:00
49fbf6735d Added tournament backups to the database and added pages to restore and create backups 2025-01-04 16:27:52 -05:00
698576dac9 Fixed the POSt endpoint to accept bracket positions 2025-01-04 16:26:57 -05:00
e986ce225b Use json for tournament backups and imports 2024-12-26 18:12:44 -05:00
15bf39014f Fix advance wrestler not running if the winner id changes 2024-12-23 23:33:55 -05:00
13bb8067fb Added logic for 32 man bracket 2024-12-23 22:12:44 -05:00
4f0f69223d Updated double elim match generation and loser name to use hashes instead of structs as well as fixing the matchup of 4v13 and 3v14 in 16 man brackets. 2024-12-23 18:27:49 -05:00
7e6d7ddfbb Updated seeding logic for double elim to make byes truly random and to keep bracket lines consistent through match generations 2024-12-23 16:17:43 -05:00
6242100e01 Removed a puts for diagnostics 2024-12-16 00:23:19 -05:00
d34fd873c0 Fixed pool order by only getting team points from pool wins and fixing calculating pin time 2024-12-16 00:19:30 -05:00
a851436c0c Removed single weight match generation, fixed bracket names, reload weight matches for pool order, and auto delete errrored jobs 2024-12-16 00:02:30 -05:00
5f049793c8 Added some cypress tests 2024-12-16 00:01:11 -05:00
fc43de71bb Clear cache for weight on pool order for bracket page 2024-12-09 08:29:52 -05:00
5219c2b1e3 Added time saver for girls middle school weight classes 2024-12-08 19:48:59 -05:00
f18802a933 Fixed double elim bracket views, fixed double elim match generation and loser name generation for 4 man bracket, fixed seed page submission and added tests, added tests for tournament generation errors and added a new error for assigning a seed higher than the number of wrestlers. 2024-12-08 19:29:56 -05:00
f6ef471591 Updated stats for new scoring 2024-12-06 07:27:58 -05:00
f92faf4f5c Updated to rails 7.2.2 2024-12-05 17:18:29 -05:00
db440c0603 Consolidated the double elimination bracket view, the double elimination match generation, and the double elimination loser names 2024-12-04 17:01:02 -05:00
5d37e5e0d8 Fixed migration for mat assignment rule 2024-11-25 20:12:36 -05:00
ad8e486205 Added mat assignment rules for the bout board and fixed the bug where a delegate making the tournamnet info public changes them to the owner 2024-11-25 16:25:59 -05:00
bb548be81b Remove influxdb errors in non prod environments 2024-11-25 11:33:47 -05:00
b1f8cc3532 Added a replica mariadb for a dr environment 2024-11-07 13:26:15 -05:00
b974773289 Added files for rvm, removed old files for Heroku newrelic and CircleCI, updated gems, and updated to rubuy 3.2 2024-10-27 16:27:48 -04:00
1f18f338ff Stop using CDN for bootstrap and datatables. Continue for font awesome, I could not get it working locally. 2024-03-21 07:45:31 -04:00
6cb523e350 config.load_defaults is now 7.1 and I had to add optional belongs to to a couple models as a deprecation from the 4.2 to 5.0 upgrade 2024-03-19 08:36:33 -04:00
b108545034 Updated name of tournaments in seeds and added local cypress tests 2024-03-16 19:06:40 -04:00
e46180e9ea Updated to rails 7.1 and ruby 3.1.4 2024-03-14 14:09:09 -04:00
fc3623008b Fixed slowlog path on mariadb 2023-03-07 17:34:14 +00:00
508dd493a1 Added slowlog to mariadb 2023-03-07 13:59:46 +00:00
92bd1ec3c9 Fixed yaml error in k8s manifest, added a passenger pool size secret, and added multiple workers 2023-03-01 21:58:24 +00:00
87353da05e Updated to rails 6.1.7.2 2023-03-01 21:58:24 +00:00
57baa3ea45 Remove tty from docker commands in test deploy 2023-02-02 13:37:13 +00:00
2eb38ce788 Allow decimals in the weight max form and remove references to lbs in views 2023-02-02 13:36:52 +00:00
e047383fe4 Added first cypress e2e tests 2023-02-02 13:16:26 +00:00
05b42dbf0e Changed weights max to decimal 2023-02-02 13:16:13 +00:00
f011dae730 Seed the test db every deploy 2023-02-02 13:11:16 +00:00
1078dc9e97 Fixed caching of school view 2023-02-02 13:11:01 +00:00
c133a8b051 Added safeguards for generating pool matches along with tests, updated two errors on about page, and added time saver to create middle school weights 2023-01-24 22:56:22 +00:00
1d0cff0e6f Pool order tests are not asserting properly and once fixed some were failing. All tests fixed and passing now. 2023-01-23 22:14:00 +00:00
1ee886abd3 Fixed creating a teampointadjust with a wrestler 2023-01-19 22:48:41 +00:00
86bb0b8410 Fixed spelling of Neutral 2023-01-19 22:48:41 +00:00
c3480909a2 Fixed school show caching 2023-01-19 01:47:32 +00:00
bde2b6d8c4 Added tests for assign_next_match mat action permissions 2023-01-09 19:55:19 -05:00
288c144f53 Added permissions tests for mat#assign_next_match 2023-01-09 19:33:41 -05:00
371b44977f Redirect errors on match#stat back to match#stat 2023-01-09 19:27:31 -05:00
eb56b9d16c Clear the cache of wrestlers when next matches are assigned to a mat 2023-01-09 19:19:30 -05:00
6b57246080 Fixed the mat#show route by defining the correct vars in the controller and added a route for assign_next_match on mat 2023-01-09 19:16:36 -05:00
ffb7d8be5b Always clear the cache for wrestlers when the match changes 2023-01-09 19:15:51 -05:00
22f5733160 Fixed export bug when adding overtime type to matches 2023-01-05 22:47:29 -05:00
b5ab929270 Hide one more text on tournament show page depending on if tournament is public or not 2023-01-02 22:08:31 -05:00
1d646cb05d Do not show brackets or bout board if you tournament is not public 2023-01-02 15:57:08 -05:00
c328bbd91c Added is_public to a tournament to hide lineups and brackets until you're ready to make it public 2023-01-01 23:16:12 -05:00
d675337d7a Updated gems 2022-12-21 22:04:44 +00:00
2dccf8dd55 Using a proxy for plausible 2022-10-05 00:46:39 +00:00
6eb71ef59e Using a proxy for plausible 2022-10-04 22:54:43 +00:00
67bee921ab Using a proxy for plausible 2022-10-04 22:06:38 +00:00
8672bdd73d Moved from google analytics to plausible 2022-10-04 13:39:47 +00:00
9c1563febf Do not initialize InfluxDB::Rails unless the host environment variable is defined 2022-09-16 19:21:58 +00:00
994fc18365 Added overtime_type to matches 2022-09-13 14:42:40 +00:00
645fb59e5b Fixed tournament search to search each term for both date and name 2022-09-08 14:13:21 +00:00
05acebbf5b Updated passenger pools for test deploy and moved workers to statefulsets in production 2022-08-15 23:26:44 +00:00
9c5ac6c1aa Updated to rails 6.1.6.1 2022-07-12 22:14:34 +00:00
85b0da6a30 Upgraded to rails 6.1.6 2022-07-07 11:58:20 +00:00
907a2eadef Fixed bug on matchstats where p2 was out of line and time_ago_in_words wasn't working 2022-04-29 11:09:22 +00:00
e6797fcce9 Remove therubyracer 2022-04-28 11:22:04 +00:00
add48b95c3 Took away cpu limits due to recommendations in the industry and moved from the beta to v1 ingress apis in K8s 2022-04-28 00:54:03 +00:00
85701e194a Upgraded to rails 6.1.5.1 and Ruby 3.0.4 2022-04-28 00:53:18 +00:00
4d3f93a109 Updated to rails 6.1.5 2022-03-23 19:36:55 +00:00
f9095d8c99 Upgrading circleci machine 2022-03-23 17:43:49 +00:00
a3724914aa Print before import 2022-02-25 13:07:54 +00:00
6e712cd199 Added caching to the school#show page 2022-02-23 21:25:01 +00:00
a3391afe02 Added indexs for matches on weight_id and wrestlers on school_id 2022-02-23 21:25:01 +00:00
cce2e5b5f8 When delegating, only directly match the email to not expose user emails 2022-02-23 21:25:01 +00:00
2cd62bbbd5 Added P2 to stats page 2022-02-23 21:25:00 +00:00
d6a273c964 Fixed the multiplication expr 2022-02-07 13:03:23 +00:00
78bd51cafb export the passenger pool size 2022-02-04 13:29:00 +00:00
ae70128479 Docker compose deploys use the correct passenger workers 2022-02-03 21:33:24 +00:00
9fec6c079f Fixed the stats page 2022-02-03 21:33:24 +00:00
58d088907a Allow nil when modifiying match wrestlers, allow nil when stating a match, fixed tournament navbar on wrestler edit, fixed tournament navbar on edit match wrestlers 2022-01-24 21:54:50 +00:00
7220ffe3c6 Fixed first round bracket name for 8 man brackets 2022-01-22 03:48:50 +00:00
0c9349c871 Moved some text on the tournament show page 2022-01-21 20:45:10 +00:00
d42b683f67 Added a sort by bout number for assign_next_match 2022-01-19 03:28:16 +00:00
f0e8c99b9f Use match id instead of bout number when looking stuff up on bracket view. 2021-12-22 13:13:44 +00:00
6db1dacbb6 Bracket name should not only rely on the round number 2021-12-22 13:12:59 +00:00
eae2c6756c Added 7/8 place match for double elimination bracket view 2021-12-22 13:12:59 +00:00
194fbca978 Updating calculating team scores as a put 2021-12-21 02:38:43 +00:00
1c8e3af5f4 Fixed navbar on school show page 2021-12-21 02:04:08 +00:00
b571bcc749 Fix calculate team scores 2021-12-21 01:23:28 +00:00
a6cc05424c Do not award bye points if they get bye's all the way to the finals and win by bye 2021-12-21 00:39:40 +00:00
efeea574ed Upgraded to rails 6.1.4.4 2021-12-21 00:29:42 +00:00
54e755ba3a Added bye points for double elimination and modified double elimination 2021-12-20 20:48:29 +00:00
8430949a37 Fixed a couple slow queries 2021-12-11 03:08:43 +00:00
5ef27f19cc Fixed ad blocker checker 2021-12-11 03:08:43 +00:00
09651ff12e Updated gems 2021-12-11 03:08:43 +00:00
c0ad06cea7 Fixed syntax error in rake task to finish seed tournaments 2021-11-08 23:09:04 +00:00
ad259f00b5 Fixed n+1 error for matches page on tournament 2021-11-08 22:42:55 +00:00
7ee8abe81a Updated db seeding for all tournament types 2021-11-08 22:42:16 +00:00
cc38c842e0 Updated manifest for mariadb to include backup and metrics 2021-10-21 23:56:49 +00:00
f7ea68da17 Updated gems and rails for vulnerabilities, switched to bundle-audit from hakiri due to hakiri not supporting ruby 3, and removed the travisci gem. 2021-08-30 20:06:31 +00:00
cb5f0e28ae Fixed the CI links on README 2021-08-20 20:29:35 +00:00
873e5b9465 Fixed development compose file syntax error 2021-05-01 13:17:29 +00:00
df6a029ef1 Persist influxdb on dev docker compose 2021-04-01 21:45:47 +00:00
03d884e672 Updated rails to 6.1.3.1 to fix the mimemagic issues 2021-04-01 20:06:51 +00:00
e1cba865b0 Added influxdb to wrestlingdev 2021-04-01 19:52:18 +00:00
e36de59971 Mispelled include for fixing bracket partial bug 2021-03-03 00:39:34 +00:00
329c01db79 Added 1 more hint for tournament types 2021-03-03 00:38:01 +00:00
4cdc9e7df1 Split bracket integration tests into a logical separation 2021-03-03 00:24:14 +00:00
1f43fdf8fa Updated about page for modified 16 man bracket 2021-03-03 00:24:14 +00:00
f6aff5a753 Fixed bracket partial bug 2021-03-03 00:24:14 +00:00
3dabc16a82 Added regular double elimination placing 1-8 tournament 2021-03-01 23:55:35 +00:00
9d51ef7b51 Fixed all brackets page 2021-03-01 13:28:00 +00:00
63b0541aa4 Added a precision and scale to the school score to make sure decimals are stored in mysql 2021-02-22 16:22:06 +00:00
1e30344be8 Fixed bug where wrestler deducted points were deducting twice from the team score 2021-02-22 16:21:22 +00:00
683b2967af Fix team score to have half a point 2021-02-22 15:29:29 +00:00
432903e7a9 Fixed small bug in modified 16 man double elim brackets 2021-02-22 13:42:25 +00:00
537eccf04d Added tests for modified double elimination, added a 1-8 place modified double elimination, and renamed regular double elimination 2021-02-20 01:33:31 +00:00
89a695388a Change school abbreviation if school has 3 names 2021-02-19 17:09:47 +00:00
0aea26967a Added quick create high school girls weight classes 2021-02-18 20:27:44 +00:00
5677f4e944 Updated development to allow all hostnames (for localtunnel) 2021-02-18 20:27:44 +00:00
f5ddc6652d Trying to fix all_brackets 2021-02-17 04:00:59 +00:00
987c89b7d5 Fix all brackets view for Rails 6 2021-02-17 03:44:55 +00:00
b2ba1901df Changed forfeit bracket label to FF 2021-02-17 03:38:03 +00:00
6c86f25add Added html_safe for all_matches view 2021-02-17 03:37:52 +00:00
ce063f5faa Fixed error_string bug in match generations 2021-02-15 13:40:54 +00:00
396e4be5b3 Fix deploy job and put into workflow 2021-02-12 19:29:27 +00:00
46919a2b1b Add heroku deploy to circleci 2021-02-12 19:13:39 +00:00
55d122771c Had to create storage.yml for activestorage for Rails 6.1 upgrade and changed config to use mem_cache_store instead of dalli_store 2021-02-12 13:16:30 +00:00
e43f3253d4 Updated README for new versions of rails and ruby 2021-02-12 12:54:00 +00:00
f720de0db6 Fixed deprecations for rails 6.1 and 6.2 2021-02-12 12:17:46 +00:00
44c9f947d7 Use user_id for dev script 2021-02-11 16:39:29 +00:00
ee8d861bee Updated to rails 6.1.2 and updated all gems 2021-02-11 16:39:17 +00:00
bdd80fc1d2 Updated to ruby 3.0.0 2021-02-11 12:33:25 +00:00
db15e79c0f Updated to rails 6.0.3.4 2020-11-10 15:58:26 -05:00
1f5aa304ff Updated to ruby 2.7.2 and rails 6.0.3.3 2020-11-10 15:45:30 -05:00
4522113396 Added status badge for circleci and removed travisci config 2020-11-10 15:44:51 -05:00
7327902fe8 Merge pull request #13 from jcwimer/circleci-project-setup
Add .circleci/config.yml
2020-11-06 12:29:02 -05:00
382 changed files with 40483 additions and 15658 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,10 +0,0 @@
version: 2
jobs:
build:
machine: true
steps:
- checkout
# start proprietary DB using private Docker image
# with credentials stored in the UI
- run: |
bash bin/run-tests-with-docker.sh

11
.cursorrules Normal file
View 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
View File

@@ -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
View File

@@ -0,0 +1 @@
wrestlingdev

1
.ruby-version Normal file
View File

@@ -0,0 +1 @@
ruby-3.2.0

View File

@@ -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

73
Gemfile
View File

@@ -1,23 +1,36 @@
source 'https://rubygems.org'
ruby '2.6.5'
ruby '3.2.0'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '6.0.3.2'
# Use sqlite3 as the database for Active Record
#gem 'sqlite3', '~> 1.3', '< 1.4', :group => :development
gem 'sqlite3', :group => :development
gem 'rails', '8.0.3'
# 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,46 @@ 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'
# 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'
# Interface for viewing and managing background jobs
# gem 'delayed_job_web'
# Note: solid_queue-ui is not compatible with Rails 8.0 yet
# We'll create a custom UI or wait for compatibility updates
# gem 'solid_queue_ui', '~> 0.1.1'
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

View File

@@ -1,342 +1,368 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (6.0.3.2)
actionpack (= 6.0.3.2)
actioncable (8.0.3)
actionpack (= 8.0.3)
activesupport (= 8.0.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.2)
actionpack (= 6.0.3.2)
activejob (= 6.0.3.2)
activerecord (= 6.0.3.2)
activestorage (= 6.0.3.2)
activesupport (= 6.0.3.2)
mail (>= 2.7.1)
actionmailer (6.0.3.2)
actionpack (= 6.0.3.2)
actionview (= 6.0.3.2)
activejob (= 6.0.3.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.2)
actionview (= 6.0.3.2)
activesupport (= 6.0.3.2)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.2)
actionpack (= 6.0.3.2)
activerecord (= 6.0.3.2)
activestorage (= 6.0.3.2)
activesupport (= 6.0.3.2)
zeitwerk (~> 2.6)
actionmailbox (8.0.3)
actionpack (= 8.0.3)
activejob (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
mail (>= 2.8.0)
actionmailer (8.0.3)
actionpack (= 8.0.3)
actionview (= 8.0.3)
activejob (= 8.0.3)
activesupport (= 8.0.3)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.3)
actionview (= 8.0.3)
activesupport (= 8.0.3)
nokogiri (>= 1.8.5)
actionview (6.0.3.2)
activesupport (= 6.0.3.2)
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.0.3)
actionpack (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.3)
activesupport (= 8.0.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.2)
activesupport (= 6.0.3.2)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.3)
activesupport (= 8.0.3)
globalid (>= 0.3.6)
activemodel (6.0.3.2)
activesupport (= 6.0.3.2)
activerecord (6.0.3.2)
activemodel (= 6.0.3.2)
activesupport (= 6.0.3.2)
activestorage (6.0.3.2)
actionpack (= 6.0.3.2)
activejob (= 6.0.3.2)
activerecord (= 6.0.3.2)
marcel (~> 0.3.1)
activesupport (6.0.3.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.4.0)
ast (2.4.1)
backports (3.18.2)
bcrypt (3.1.16)
brakeman (4.9.1)
builder (3.2.4)
bullet (6.1.0)
activemodel (8.0.3)
activesupport (= 8.0.3)
activerecord (8.0.3)
activemodel (= 8.0.3)
activesupport (= 8.0.3)
timeout (>= 0.4.0)
activestorage (8.0.3)
actionpack (= 8.0.3)
activejob (= 8.0.3)
activerecord (= 8.0.3)
activesupport (= 8.0.3)
marcel (~> 1.0)
activesupport (8.0.3)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
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.20)
benchmark (0.4.1)
bigdecimal (3.3.0)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.1.0)
racc
builder (3.3.0)
bullet (8.0.8)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
cancancan (3.1.0)
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.7)
bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
cancancan (3.6.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
crass (1.0.6)
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.2)
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.3)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.14.0)
faraday (>= 0.7.4, < 1.0)
ffi (1.13.1)
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.8.5)
daemons (1.4.1)
date (3.4.1)
drb (2.2.3)
erb (5.0.3)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
fugit (1.11.2)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
globalid (1.3.0)
activesupport (>= 6.1)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jbuilder (2.10.1)
activesupport (>= 5.0.0)
jquery-rails (4.4.0)
importmap-rails (2.2.2)
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.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.14.1)
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
jquery-rails (4.6.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.1)
launchy (2.4.3)
addressable (~> 2.3)
libv8 (3.16.14.19)
loofah (2.7.0)
json (2.15.1)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
method_source (1.0.0)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.0512)
mimemagic (0.3.5)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.2)
multi_json (1.15.0)
multipart-post (2.1.1)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
mysql2 (0.5.3)
net-http-persistent (2.9.4)
net-http-pipeline (1.0.1)
netrc (0.11.0)
newrelic_rpm (6.12.0.367)
nio4r (2.5.3)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
orm_adapter (0.5.0)
parallel (1.19.2)
parser (2.7.1.4)
net-imap
net-pop
net-smtp
marcel (1.1.0)
mini_mime (1.1.5)
minitest (5.25.5)
mocha (2.7.1)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
mysql2 (0.5.7)
bigdecimal
net-imap (0.5.12)
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.4)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl)
racc (~> 1.4)
parallel (1.27.0)
parser (3.3.9.0)
ast (~> 2.4.1)
passenger (6.0.6)
racc
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.5.1)
propshaft (1.3.1)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
rake (>= 0.8.1)
puma (4.3.6)
psych (5.2.6)
date
stringio
puma (7.0.4)
nio4r (~> 2.0)
pusher-client (0.6.2)
json
websocket (~> 1.0)
rack (2.2.3)
rack-protection (2.1.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.3.2)
actioncable (= 6.0.3.2)
actionmailbox (= 6.0.3.2)
actionmailer (= 6.0.3.2)
actionpack (= 6.0.3.2)
actiontext (= 6.0.3.2)
actionview (= 6.0.3.2)
activejob (= 6.0.3.2)
activemodel (= 6.0.3.2)
activerecord (= 6.0.3.2)
activestorage (= 6.0.3.2)
activesupport (= 6.0.3.2)
bundler (>= 1.3.0)
railties (= 6.0.3.2)
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.2)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.3)
actioncable (= 8.0.3)
actionmailbox (= 8.0.3)
actionmailer (= 8.0.3)
actionpack (= 8.0.3)
actiontext (= 8.0.3)
actionview (= 8.0.3)
activejob (= 8.0.3)
activemodel (= 8.0.3)
activerecord (= 8.0.3)
activestorage (= 8.0.3)
activesupport (= 8.0.3)
bundler (>= 1.15.0)
railties (= 8.0.3)
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.3.2)
actionpack (= 6.0.3.2)
activesupport (= 6.0.3.2)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
rake (13.0.1)
railties (8.0.3)
actionpack (= 8.0.3)
activesupport (= 8.0.3)
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.0)
rb-readline (0.5.5)
rdoc (6.2.1)
ref (2.0.0)
regexp_parser (1.7.1)
responders (3.0.1)
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)
rexml (3.2.4)
round_robin_tournament (0.0.1)
rubocop (0.91.0)
rdoc (6.15.0)
erb
psych (>= 4.0.0)
tsort
regexp_parser (2.11.3)
reline (0.6.2)
io-console (~> 0.5)
round_robin_tournament (0.1.2)
rubocop (1.81.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 2.7.1.1)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml
rubocop-ast (>= 0.4.0, < 1.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.4.0)
parser (>= 2.7.1.4)
ruby-progressbar (1.10.1)
ruby2_keywords (0.0.2)
sdoc (1.1.0)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.47.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
sdoc (2.6.4)
rdoc (>= 5.0)
sinatra (2.1.0)
mustermann (~> 1.0)
rack (~> 2.2)
rack-protection (= 2.1.0)
tilt (~> 2.0)
spring (2.1.1)
sprockets (4.0.2)
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.7)
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.2.1)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
fugit (~> 1.11.0)
railties (>= 7.1)
thor (>= 1.3.1)
spring (4.4.0)
sqlite3 (2.7.4-aarch64-linux-gnu)
sqlite3 (2.7.4-aarch64-linux-musl)
sqlite3 (2.7.4-arm-linux-gnu)
sqlite3 (2.7.4-arm-linux-musl)
sqlite3 (2.7.4-arm64-darwin)
sqlite3 (2.7.4-x86_64-darwin)
sqlite3 (2.7.4-x86_64-linux-gnu)
sqlite3 (2.7.4-x86_64-linux-musl)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.7)
thor (1.4.0)
timeout (0.4.3)
tsort (0.2.0)
turbo-rails (2.0.17)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.4.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
therubyracer (0.12.3)
libv8 (~> 3.16.14.15)
ref
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.10)
travis (1.8.13)
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.7)
thread_safe (~> 0.1)
tzinfo-data (1.2020.1)
tzinfo-data (1.2025.2)
tzinfo (>= 1.0.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
uniform_notifier (1.13.0)
warden (1.2.9)
rack (>= 2.0.9)
websocket (1.2.8)
websocket-driver (0.7.3)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uniform_notifier (1.18.0)
uri (1.0.4)
useragent (0.16.11)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.4.0)
zeitwerk (2.7.3)
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
mocha
mysql2
newrelic_rpm
passenger
propshaft
puma
rails (= 6.0.3.2)
rails (= 8.0.3)
rails-controller-testing
rails-html-sanitizer
rails_12factor
rb-readline
round_robin_tournament
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 3.2.0p0
BUNDLED WITH
2.0.2
2.6.9

View File

@@ -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

220
README.md
View File

@@ -1,56 +1,212 @@
# README
This application is being created to run a wrestling tournament.
### Current master status
[![Build Status](https://travis-ci.org/jcwimer/wrestlingApp.svg?branch=master)](https://travis-ci.org/jcwimer/wrestlingApp)
### Current development status
[![Build Status](https://travis-ci.org/jcwimer/wrestlingApp.svg?branch=development)](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 3.2.0
* Rails 8.0.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`
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/'"`
## 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
```
## 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
### 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`

View File

@@ -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

View File

@@ -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/

View File

@@ -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/

View File

@@ -1,16 +1,47 @@
// 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";
import "datatables.net";
// 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";
// Register controllers
application.register("wrestler-color", WrestlerColorController);
application.register("match-score", MatchScoreController);
application.register("match-data", MatchDataController);
application.register("match-spectate", MatchSpectateController);
// 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, Stimulus, and DataTables.");
// 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";

View File

@@ -0,0 +1,384 @@
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.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)
// 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()
// 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}`)
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')
}
},
disconnected: () => {
console.log(`[Stats AC] Disconnected from MatchStatsChannel`)
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)}...`)
this.w1.stats = data.w1_stat
this.w1StatTarget.value = data.w1_stat
}
// Update w2 stats
if (data.w2_stat !== undefined && this.w2StatTarget) {
console.log(`[Stats AC] Updating w2_stat: ${data.w2_stat.substring(0, 30)}...`)
this.w2.stats = data.w2_stat
this.w2StatTarget.value = data.w2_stat
}
},
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')
}
}
}
)
}
}

View 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();
}
}
}

View File

@@ -0,0 +1,134 @@
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')
}
},
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'
}
}
}

View File

@@ -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")
}
}
})
}
}

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -1,4 +0,0 @@
/*
Place all the styles related to the matching controller here.
They will automatically be included in application.css.
*/

View File

@@ -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");
}

View File

@@ -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;
}
}

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -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/

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@@ -0,0 +1,63 @@
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
end

View File

@@ -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

View File

@@ -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

View 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
@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

View File

@@ -1,5 +1,5 @@
class MatchesController < ApplicationController
before_action :set_match, only: [:show, :edit, :update, :destroy, :stat]
before_action :set_match, only: [:show, :edit, :update, :stat, :spectate]
before_action :check_access, only: [:edit,:update, :stat]
# GET /matches/1
@@ -16,41 +16,99 @@ 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
# @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
session[:return_path] = "/tournaments/#{@tournament.id}/matches"
session[:error_return_path] = "/matches/#{@match.id}/stat"
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
# 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 +119,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

View File

@@ -1,19 +1,53 @@
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 :set_mat, only: [:show, :edit, :update, :destroy, :assign_next_match]
before_action :check_access, only: [:new,:create,:update,:destroy,:edit,:show, :assign_next_match]
before_action :check_for_matches, only: [:show]
# GET /mats/1
# GET /mats/1.json
def show
@match = @mat.unfinished_matches.first
if @match
@w1 = @match.wrestler1
@w2 = @match.wrestler2
@wrestlers = [@w1,@w2]
bout_number_param = params[:bout_number] # Read the bout_number from the URL params
if bout_number_param
@show_next_bout_button = false
@match = @mat.unfinished_matches.find { |m| m.bout_number == bout_number_param.to_i }
else
@show_next_bout_button = true
@match = @mat.unfinished_matches.first
end
@next_match = @mat.unfinished_matches.second # Second unfinished match on the mat
@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 +78,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.assign_next_match
format.html { redirect_to "/tournaments/#{@mat.tournament.id}", notice: "Next Match on Mat #{@mat.name} successfully completed." }
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

View 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

View File

@@ -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

View 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

View 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

View File

@@ -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]
before_action :check_access_manage, only: [:delete_school_keys, :generate_school_keys,:reset_bout_board,:calculate_team_scores,:swap,:weigh_in_sheet,:teampointadjust,:remove_teampointadjust,:remove_school_delegate,:school_delegate,:weigh_in,:weigh_in_weight,:create_custom_weights,:update,:edit,:generate_matches,:matches]
before_action :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,:up_matches,:bracket,:all_brackets]
before_action :check_access_read, only: [:all_results,:up_matches,:bracket,:all_brackets]
def weigh_in_sheet
end
def export
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,18 @@ 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
@users_delegates = []
@tournament.schools.each do |s|
s.delegates.each do |d|
@users_delegates << d
end
end
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 +114,12 @@ 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
end
def matches
@matches = @tournament.matches.sort_by{|m| m.bout_number}
@matches = @tournament.matches.includes(:wrestlers,:schools).sort_by{|m| m.bout_number}
if @match
@w1 = @match.wrestler1
@w2 = @match.wrestler2
@@ -165,7 +150,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 +159,27 @@ class TournamentsController < ApplicationController
def all_brackets
@schools = @tournament.schools
@schools = @schools.sort_by{|s| s.page_score_string}.reverse!
@matches = @tournament.matches.includes(:wrestlers,:schools)
@weights = @tournament.weights.includes(:matches,:wrestlers)
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
@@ -204,7 +198,14 @@ class TournamentsController < ApplicationController
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)
# .where.not(loser1_name: 'BYE') won't return matches with NULL loser1_name
# so I was only getting back matches with Loser of BOUT_NUMBER
@matches = @tournament.matches
.where("mat_id is NULL and (finished != 1 or finished is NULL)")
.where("loser1_name != ? OR loser1_name IS NULL", "BYE")
.where("loser2_name != ? OR loser2_name IS NULL", "BYE")
.order('bout_number ASC')
.limit(10).includes(:wrestlers)
@mats = @tournament.mats.includes(:matches)
end
@@ -220,17 +221,33 @@ class TournamentsController < ApplicationController
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 +263,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 +298,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 +335,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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,32 @@
class AdvanceWrestlerJob < ApplicationJob
queue_as :default
def perform(wrestler, match)
# 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

View 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

View File

@@ -0,0 +1,38 @@
class CalculateSchoolScoreJob < ApplicationJob
queue_as :default
# 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

View File

@@ -0,0 +1,20 @@
class GenerateTournamentMatchesJob < ApplicationJob
queue_as :default
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

View File

@@ -0,0 +1,32 @@
class TournamentBackupJob < ApplicationJob
queue_as :default
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

View File

@@ -0,0 +1,36 @@
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
# 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

View File

@@ -0,0 +1,33 @@
class WrestlingdevImportJob < ApplicationJob
queue_as :default
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

View File

@@ -0,0 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: ENV["WRESTLINGDEV_EMAIL"] || 'noreply@wrestlingdev.com'
layout 'mailer'
end

View 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

View File

@@ -1,57 +1,74 @@
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 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
else
# NON LOGGED IN USER PERMISSIONS
# TOURNAMENT PERMISSIONS
# Can read tournament if tournament is public
can :read, Tournament do |tournament|
tournament.is_public
end
cannot :destroy, School do |school|
school.delegates.map(&:user_id).include? user.id
# SCHOOL PERMISSIONS
# wrestler permissions are included with school permissions
# 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 read 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
end

View File

@@ -0,0 +1,3 @@
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end

View File

@@ -1,6 +1,7 @@
class Mat < ActiveRecord::Base
class Mat < ApplicationRecord
belongs_to :tournament
has_many :matches
has_many :matches, dependent: :destroy
has_many :mat_assignment_rules, dependent: :destroy
validates :name, presence: true
@@ -21,14 +22,65 @@ class Mat < ActiveRecord::Base
end
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 = next_eligible_match
self.matches.reload
if match and self.unfinished_matches.size < 4
match.mat_id = self.id
match.save
if match.save
# Invalidate any wrestler caches
if match.w1
match.wrestler1.touch
match.wrestler1.school.touch
end
if match.w2
match.wrestler2.touch
match.wrestler2.school.touch
end
return true
else
return false
end
else
return true
end
end
def next_eligible_match
# Start with all matches that are either unfinished (nil or 0), have a bout number, and are ordered by bout_number
filtered_matches = tournament.matches
.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")
# 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 unfinished_matches
matches.select{|m| m.finished != 1}.sort_by{|m| m.bout_number}
end

View 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

View File

@@ -1,31 +1,95 @@
class Match < ActiveRecord::Base
class Match < ApplicationRecord
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
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
# 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
# School point calculation has move to the end of advance wrestler
# calculate_school_points
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: 1:23 or 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 +97,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 +134,7 @@ class Match < ActiveRecord::Base
sec = time.partition(':').last.to_i
return minutes_in_seconds + sec
else
nil
0
end
end
@@ -89,17 +153,19 @@ 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
@@ -137,56 +203,73 @@ class Match < ActiveRecord::Base
end
if self.w1 != nil
if self.round == 1
if self.wrestler1.original_seed
return_string = return_string + "[#{wrestler1.original_seed}] "
end
return_string = return_string + "#{w1_name} - #{wrestler1.school.name} (#{wrestler1.season_win}-#{wrestler1.season_loss})"
return_string = return_string + "#{wrestler1.long_bracket_name}"
else
return_string = return_string + "#{w1_name} (#{wrestler1.school.abbreviation})"
return_string = return_string + "#{wrestler1.short_bracket_name}"
end
else
return_string = return_string + "#{w1_name}"
return_string = return_string + "#{self.loser1_name}"
end
return return_string + return_string_ending
end
def w2_bracket_name
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 == 1
if self.wrestler2.original_seed
return_string = return_string + "#{wrestler2.original_seed} "
end
return_string = return_string + "#{w2_name} - #{wrestler2.school.name} (#{wrestler2.season_win}-#{wrestler2.season_loss})"
else
return_string = return_string + "#{w2_name} (#{wrestler2.school.abbreviation})"
end
else
return_string = return_string + "#{w2_name}"
end
return return_string + return_string_ending
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 == 1
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
@@ -239,4 +322,16 @@ 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
end

View File

@@ -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

View File

@@ -1,4 +1,4 @@
class SchoolDelegate < ActiveRecord::Base
class SchoolDelegate < ApplicationRecord
belongs_to :school
belongs_to :user
end

View File

@@ -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

View File

@@ -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,17 +45,27 @@ 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 create_pre_defined_weights(value)
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
if value == 'hs'
Weight::HS_WEIGHT_CLASSES.each do |w|
weights.create(max: w)
end
else
raise "Unspecified behavior"
weight_classes.each do |w|
weights.create(max: w)
end
end
@@ -52,11 +78,8 @@ 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
# 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 assign_mats(mats_to_assign)
@@ -101,22 +124,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|
@@ -129,13 +154,13 @@ class Tournament < ActiveRecord::Base
return error_string
end
def double_elim_number_of_wrestlers
def double_elim_number_of_wrestlers_error
error_string = ""
if self.tournament_type == "Double Elimination 1-6"
weights_with_too_many_wrestlers = weights.select{|w| w.wrestlers.size > 16}
if self.tournament_type == "Double Elimination 1-6" or self.tournament_type == "Double Elimination 1-8"
weights_with_too_many_wrestlers = weights.select{|w| w.wrestlers.size > 64}
weight_with_too_few_wrestlers = weights.select{|w| w.wrestlers.size < 4}
weights_with_too_many_wrestlers.each do |weight|
error_string = error_string + " The weight class #{weight.max} has more than 16 wrestlers."
error_string = error_string + " The weight class #{weight.max} has more than 64 wrestlers."
end
weight_with_too_few_wrestlers.each do |weight|
error_string = error_string + " The weight class #{weight.max} has less than 4 wrestlers."
@@ -143,23 +168,115 @@ class Tournament < ActiveRecord::Base
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
double_elim_error = double_elim_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
elsif double_elim_error.length > 0
return error_string + double_elim_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
if mats.any?
4.times do
# Iterate over each mat and assign the next available match
mats.each do |mat|
match_assigned = mat.assign_next_match
# If no more matches are available, exit early
unless match_assigned
puts "No more eligible matches to assign."
return
end
end
end
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

View File

@@ -0,0 +1,5 @@
class TournamentBackup < ApplicationRecord
belongs_to :tournament
validates :backup_data, presence: true
end

View File

@@ -1,4 +1,4 @@
class TournamentDelegate < ActiveRecord::Base
class TournamentDelegate < ApplicationRecord
belongs_to :tournament
belongs_to :user
end

View 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

View File

@@ -1,12 +1,56 @@
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 = []

View File

@@ -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.reload.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

View File

@@ -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

View File

@@ -1,22 +1,25 @@
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)
end
def advance_raw
@last_match.reload
@wrestler.reload
if @last_match && @last_match.finished?
pool_to_bracket_advancement if @tournament.tournament_type == "Pool to bracket"
DoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type == "Modified 16 Man Double Elimination" or
@tournament.tournament_type == "Double Elimination 1-6"
ModifiedDoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
DoubleEliminationAdvance.new(@wrestler, @last_match).bracket_advancement if @tournament.tournament_type.include? "Regular Double Elimination"
end
@wrestler.school.calculate_score
end
def pool_to_bracket_advancement

View File

@@ -7,62 +7,52 @@ class DoubleEliminationAdvance
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 = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?",next_match_bracket_position,next_match_position_number.ceil,@wrestler.weight_id).first
end
if next_match
update_new_match(next_match, get_wrestler_number)
end
end
@@ -92,26 +82,39 @@ 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 = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?", "Loser of #{bout}", "Loser of #{bout}", @wrestler.weight_id).first
if next_match.present?
next_match.replace_loser_name_with_wrestler(@wrestler, "Loser of #{bout}")
next_match.reload
if next_match.loser1_name == "BYE" || next_match.loser2_name == "BYE"
next_match.winner_id = @wrestler.id
next_match.win_type = "BYE"
next_match.score = ""
next_match.finished = 1
# puts "Before save: winner_id=#{next_match.winner_id}"
# if next_match.save
# puts "Save successful: winner_id=#{next_match.reload.winner_id}"
# else
# puts "Save failed: #{next_match.errors.full_messages}"
# end
next_match.save
next_match.advance_wrestlers
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|
match.finished = 1
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}
@@ -134,4 +137,26 @@ class DoubleEliminationAdvance
match.save
end
end
def set_bye_for_placement
weight = @wrestler.weight
fifth_finals = weight.matches.select{|match| match.bracket_position == '5/6'}.first
seventh_finals = weight.matches.select{|match| match.bracket_position == '7/8'}.first
if seventh_finals
conso_quarter = weight.matches.select{|match| match.bracket_position == 'Conso Quarter'}
conso_quarter.each do |match|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
seventh_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
end
end
end
if fifth_finals
conso_semis = weight.matches.select{|match| match.bracket_position == 'Conso Semis'}
conso_semis.each do |match|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
fifth_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
end
end
end
end
end

View File

@@ -0,0 +1,144 @@
class ModifiedDoubleEliminationAdvance
def initialize(wrestler,last_match)
@wrestler = wrestler
@last_match = last_match
@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 = 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"
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)
elsif @last_match.bracket_position == "Conso Quarter"
# it's a special bracket where a semi loser is not dropping down
new_match = Match.where("bracket_position = ? AND bracket_position_number = ? AND weight_id = ?","Conso Semis",@next_match_position_number.ceil,@wrestler.weight_id).first
update_new_match(new_match, get_wrestler_number)
elsif @last_match.bracket_position == "Bracket Round of 16"
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and bracket_position = ?", @next_match_position_number.ceil,@wrestler.weight_id, @last_match.round , "Quarter").sort_by{|m| m.round}.first
update_new_match(new_match, get_wrestler_number)
elsif @last_match.bracket_position == "Conso Round of 8"
new_match = Match.where("bracket_position_number = ? and weight_id = ? and round > ? and bracket_position = ?", @last_match.bracket_position_number,@wrestler.weight_id, @last_match.round, "Conso Quarter").sort_by{|m| m.round}.first
update_new_match(new_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}")
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 = Match.where("(loser1_name = ? OR loser2_name = ?) AND weight_id = ?", "Loser of #{bout}", "Loser of #{bout}", @wrestler.weight_id).first
if next_match.present?
next_match.replace_loser_name_with_wrestler(@wrestler, "Loser of #{bout}")
next_match.reload
if next_match.loser1_name == "BYE" || next_match.loser2_name == "BYE"
next_match.winner_id = @wrestler.id
next_match.win_type = "BYE"
next_match.score = ""
next_match.finished = 1
# puts "Before save: winner_id=#{next_match.winner_id}"
# if next_match.save
# puts "Save successful: winner_id=#{next_match.reload.winner_id}"
# else
# puts "Save failed: #{next_match.errors.full_messages}"
# end
next_match.save
next_match.advance_wrestlers
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|
match.finished = 1
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 }
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
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
next_match.loser2_name = "BYE"
else
next_match.loser1_name = "BYE"
end
end
next_match.save
match.save
end
end
def set_bye_for_placement
weight = @wrestler.weight
seventh_finals = weight.matches.select{|match| match.bracket_position == '7/8'}.first
if seventh_finals
conso_quarter = weight.matches.select{|match| match.bracket_position == 'Conso Semis'}
conso_quarter.each do |match|
if match.loser1_name == "BYE" or match.loser2_name == "BYE"
seventh_finals.replace_loser_name_with_bye("Loser of #{match.bout_number}")
end
end
end
end
end

View File

@@ -30,15 +30,20 @@ class PoolAdvance
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
updateNewMatch(new_match)
@@ -65,7 +70,7 @@ class PoolAdvance
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

View File

@@ -4,36 +4,48 @@ class PoolOrder
end
def getPoolOrder
# clear caching for weight for bracket page
@wrestlers.first.weight.touch
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
wrestler.save
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|
matches = w.reload.all_matches
w.pool_placement_tiebreaker = nil
w.pool_placement = nil
w.poolAdvancePoints = w.pool_wins.size
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 +53,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 +104,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 +120,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 +159,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 +173,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)

View File

@@ -1,13 +1,205 @@
class DoubleEliminationGenerateLoserNames
def initialize( tournament )
@tournament = tournament
def initialize(tournament)
@tournament = tournament
end
# Entry point: assign loser placeholders and advance any byes
def assign_loser_names
@tournament.weights.each do |weight|
assign_loser_names_for_weight(weight)
advance_bye_matches_championship(weight)
advance_bye_matches_consolation(weight)
end
end
private
# Assign loser names for a single weight bracket
def assign_loser_names_for_weight(weight)
bracket_size = weight.calculate_bracket_size
matches = weight.matches.reload
num_placers = @tournament.number_of_placers
# Build dynamic round definitions
champ_rounds = dynamic_championship_rounds(bracket_size)
conso_rounds = dynamic_consolation_rounds(bracket_size)
first_round = { bracket_position: first_round_label(bracket_size) }
champ_full = [first_round] + champ_rounds
# Map championship losers into consolation slots
mappings = []
champ_full[0...-1].each_with_index do |champ_info, i|
map_idx = i.zero? ? 0 : (2 * i - 1)
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
# Apply loser-name mappings
mappings.each do |map|
champ = matches.select { |m| m.bracket_position == map[:championship_bracket_position] }
.sort_by(&:bracket_position_number)
conso = matches.select { |m| m.bracket_position == map[:consolation_bracket_position] }
.sort_by(&:bracket_position_number)
current_champ_round_index = map[:champ_round_index]
if current_champ_round_index.odd?
conso.reverse!
end
idx = 0
# Determine if this mapping is for losers from the first championship round
is_first_champ_round_feed = map[:champ_round_index].zero?
conso.each do |cm|
champ_match1 = champ[idx]
if champ_match1
if is_first_champ_round_feed && ((champ_match1.w1 && champ_match1.w2.nil?) || (champ_match1.w1.nil? && champ_match1.w2))
cm.loser1_name = "BYE"
else
cm.loser1_name = "Loser of #{champ_match1.bout_number}"
end
else
cm.loser1_name = nil # Should not happen if bracket generation is correct
end
if map[:both_wrestlers] # This is true only if is_first_champ_round_feed
idx += 1 # Increment for the second championship match
champ_match2 = champ[idx]
if champ_match2
# BYE check is only relevant for the first championship round feed
if is_first_champ_round_feed && ((champ_match2.w1 && champ_match2.w2.nil?) || (champ_match2.w1.nil? && champ_match2.w2))
cm.loser2_name = "BYE"
else
cm.loser2_name = "Loser of #{champ_match2.bout_number}"
end
else
cm.loser2_name = nil # Should not happen
end
end
idx += 1 # Increment for the next consolation match or next pair from championship
end
end
end
# 5th/6th place
if bracket_size >= 5 && num_placers >= 6
conso_semis = matches.select { |m| m.bracket_position == "Conso Semis" }
.sort_by(&:bracket_position_number)
if conso_semis.size >= 2
m56 = matches.find { |m| m.bracket_position == "5/6" }
m56.loser1_name = "Loser of #{conso_semis[0].bout_number}"
m56.loser2_name = "Loser of #{conso_semis[1].bout_number}" if m56
end
end
# 7th/8th place
if bracket_size >= 7 && num_placers >= 8
conso_quarters = matches.select { |m| m.bracket_position == "Conso Quarter" }
.sort_by(&:bracket_position_number)
if conso_quarters.size >= 2
m78 = matches.find { |m| m.bracket_position == "7/8" }
m78.loser1_name = "Loser of #{conso_quarters[0].bout_number}"
m78.loser2_name = "Loser of #{conso_quarters[1].bout_number}" if m78
end
end
matches.each(&:save!)
end
# Advance first-round byes in championship bracket
def advance_bye_matches_championship(weight)
matches = weight.matches.reload
first_round = matches.map(&:round).min
matches.select { |m| m.round == first_round }
.sort_by(&:bracket_position_number)
.each { |m| handle_bye(m) }
end
# Advance first-round byes in consolation bracket
def advance_bye_matches_consolation(weight)
matches = weight.matches.reload
bracket_size = weight.calculate_bracket_size
first_conso = dynamic_consolation_rounds(bracket_size).first
matches.select { |m| m.round == first_conso[:round] && m.bracket_position == first_conso[:bracket_position] }
.sort_by(&:bracket_position_number)
.each { |m| handle_bye(m) }
end
# Mark bye match, set finished, and advance
def handle_bye(match)
if [match.w1, match.w2].compact.size == 1
match.finished = 1
match.win_type = 'BYE'
if match.w1
match.winner_id = match.w1
match.loser2_name = 'BYE'
else
match.winner_id = match.w2
match.loser1_name = 'BYE'
end
match.score = ''
match.save!
match.advance_wrestlers
end
end
# Helpers for dynamic bracket labels
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
return '3/4'
elsif participants == 4
return j.odd? ? 'Conso Quarter' : 'Conso Semis'
else
suffix = j.odd? ? ".1" : ".2"
return "Conso Round of #{participants}#{suffix}"
end
end
end

View File

@@ -1,12 +1,239 @@
class DoubleEliminationMatchGeneration
def initialize( tournament )
@tournament = tournament
def initialize(tournament)
@tournament = tournament
end
def generate_matches
#
# PHASE 1: Generate matches (with local round definitions).
#
@tournament.weights.each do |weight|
generate_matches_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
#
# PHASE 2: Align all rounds to match the largest brackets definitions.
#
align_all_rounds_to_largest_bracket
end
###########################################################################
# PHASE 1: Generate all matches for each bracket, using a single definition.
###########################################################################
def generate_matches_for_weight(weight)
bracket_size = weight.calculate_bracket_size
bracket_info = define_bracket_matches(bracket_size)
return unless bracket_info
# 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]
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|
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|
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
create_matchup(nil, nil, "5/6", 1, round_number, weight)
end
if weight.wrestlers.size >= 7 && @tournament.number_of_placers >= 8 && matches_this_round == 1
create_matchup(nil, nil, "7/8", 1, round_number, weight)
end
end
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_all_rounds_to_largest_bracket
largest_weight = @tournament.weights.max_by { |w| w.calculate_bracket_size }
return unless largest_weight
position_to_round = {}
largest_weight.tournament.matches.where(weight_id: largest_weight.id).each do |m|
position_to_round[m.bracket_position] ||= m.round
end
@tournament.matches.find_each do |match|
if position_to_round.key?(match.bracket_position)
match.update(round: position_to_round[match.bracket_position])
end
end
end
###########################################################################
# Helper methods
###########################################################################
def wrestler_with_seed(seed, weight)
Wrestler.where("weight_id = ? AND bracket_line = ?", weight.id, seed).first&.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)
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
# 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

View File

@@ -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

View File

@@ -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

View File

@@ -3,55 +3,33 @@ 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"
ModifiedSixteenManMatchGeneration.new(@tournament).generate_matches if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
DoubleEliminationMatchGeneration.new(@tournament).generate_matches if @tournament.tournament_type.include? "Regular Double Elimination"
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"
ModifiedSixteenManGenerateLoserNames.new(@tournament).assign_loser_names if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
DoubleEliminationGenerateLoserNames.new(@tournament).assign_loser_names if @tournament.tournament_type.include? "Regular Double Elimination"
end
def standardStartingActions
@tournament.curently_generating_matches = 1
@tournament.save
WipeTournamentMatches.new(@tournament).setUpMatchGeneration
TournamentSeeding.new(@tournament).setSeeds
TournamentSeeding.new(@tournament).set_seeds
end
def postMatchCreationActions
moveFinalsMatchesToLastRound
moveFinalsMatchesToLastRound if ! @tournament.tournament_type.include? "Regular Double Elimination"
assignBouts
assignFirstMatchesToMats
@tournament.reset_and_fill_bout_board
@tournament.curently_generating_matches = nil
@tournament.save!
end
@@ -75,17 +53,6 @@ class GenerateTournamentMatches
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
end
def unAssignMats
matches = @tournament.matches.reload
matches.each do |m|

View File

@@ -10,6 +10,7 @@ class ModifiedSixteenManGenerateLoserNames
conso_round_2(matches_by_weight)
conso_round_3(matches_by_weight)
third_fourth(matches_by_weight)
seventh_eighth(matches_by_weight)
save_matches(matches_by_weight)
matches_by_weight = @tournament.matches.where(weight_id: w.id).reload
advance_bye_matches_championship(matches_by_weight)
@@ -18,25 +19,25 @@ class ModifiedSixteenManGenerateLoserNames
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|
matches.select{|m| m.bracket_position == "Conso Round of 8"}.sort_by{|m| m.bracket_position_number}.each do |match|
if match.bracket_position_number == 1
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 1 and m.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}"
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 1 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 2 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
elsif match.bracket_position_number == 2
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 3 and m.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}"
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 3 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
elsif match.bracket_position_number == 3
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 5 and m.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}"
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 5 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 6 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
elsif match.bracket_position_number == 4
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 7 and m.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}"
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 7 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position_number == 8 and m.bracket_position == "Bracket Round of 16"}.first.bout_number}"
end
end
end
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|
matches.select{|m| m.bracket_position == "Conso Quarter"}.sort_by{|m| m.bracket_position_number}.each do |match|
if match.bracket_position_number == 1
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position_number == 4 and m.bracket_position == "Quarter"}.first.bout_number}"
elsif match.bracket_position_number == 2
@@ -55,21 +56,29 @@ class ModifiedSixteenManGenerateLoserNames
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position == "Semis"}.second.bout_number}"
end
end
def seventh_eighth(matches)
matches.select{|m| m.bracket_position == "7/8"}.sort_by{|m| m.bracket_position_number}.each do |match|
match.loser1_name = "Loser of #{matches.select{|m| m.bracket_position == "Conso Semis"}.first.bout_number}"
match.loser2_name = "Loser of #{matches.select{|m| m.bracket_position == "Conso Semis"}.second.bout_number}"
end
end
def 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|
matches.select{|m| m.bracket_position == "Bracket Round of 16"}.sort_by{|m| m.bracket_position_number}.each do |match|
if match.w1 == nil or match.w2 == nil
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.score = ""
match.save
match.advance_wrestlers
elsif match.w2 != nil
match.winner_id = match.w2
match.loser1_name = "BYE"
match.score = ""
match.save
match.advance_wrestlers
end

View File

@@ -1,6 +1,7 @@
class ModifiedSixteenManMatchGeneration
def initialize( tournament )
@tournament = tournament
@number_of_placers = @tournament.number_of_placers
end
def generate_matches
@@ -18,14 +19,14 @@ class ModifiedSixteenManMatchGeneration
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)
create_matchup_from_seed(1,16, "Bracket Round of 16", 1, 1,weight)
create_matchup_from_seed(8,9, "Bracket Round of 16", 2, 1,weight)
create_matchup_from_seed(5,12, "Bracket Round of 16", 3, 1,weight)
create_matchup_from_seed(4,14, "Bracket Round of 16", 4, 1,weight)
create_matchup_from_seed(3,13, "Bracket Round of 16", 5, 1,weight)
create_matchup_from_seed(6,11, "Bracket Round of 16", 6, 1,weight)
create_matchup_from_seed(7,10, "Bracket Round of 16", 7, 1,weight)
create_matchup_from_seed(2,15, "Bracket Round of 16", 8, 1,weight)
end
def round_two(weight)
@@ -33,10 +34,10 @@ class ModifiedSixteenManMatchGeneration
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)
create_matchup(nil,nil,"Conso Round of 8",1,2,weight)
create_matchup(nil,nil,"Conso Round of 8",2,2,weight)
create_matchup(nil,nil,"Conso Round of 8",3,2,weight)
create_matchup(nil,nil,"Conso Round of 8",4,2,weight)
end
def round_three(weight)
@@ -57,6 +58,9 @@ class ModifiedSixteenManMatchGeneration
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)
if @number_of_placers >= 8
create_matchup(nil,nil,"7/8",1,5,weight)
end
end
def wrestler_with_seed(seed,weight)

View File

@@ -2,14 +2,6 @@ class PoolToBracketMatchGeneration
def initialize( tournament )
@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)
end
def generatePoolToBracketMatches
@tournament.weights.order(:max).each do |weight|

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,73 @@
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(&:attributes),
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

View File

@@ -3,42 +3,82 @@ class TournamentSeeding
@tournament = tournament
end
def setSeeds
@tournament.weights.each do |w|
resetAllSeeds(w)
setOriginalSeeds(w)
randomSeeding(w)
def set_seeds
@tournament.weights.each do |weight|
wrestlers = weight.wrestlers
bracket_size = weight.calculate_bracket_size
wrestlers = reset_bracket_line_for_lines_higher_than_bracket_size(wrestlers, bracket_size)
wrestlers = set_original_seed_to_bracket_line(wrestlers)
wrestlers = random_seeding(wrestlers, bracket_size)
wrestlers.each(&:save)
end
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
first_half_available_bracket_lines = (1..half_of_bracket).to_a
# remove bracket lines that are taken from available_bracket_lines
wrestlers_with_bracket_lines = wrestlers.select{|w| w.bracket_line != nil }
wrestlers_with_bracket_lines.each do |wrestler|
available_bracket_lines.delete(wrestler.bracket_line)
first_half_available_bracket_lines.delete(wrestler.bracket_line)
end
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 first_half_available_bracket_lines.size > 0
random_available_bracket_line = first_half_available_bracket_lines.sample
wrestler.bracket_line = random_available_bracket_line
available_bracket_lines.delete(random_available_bracket_line)
first_half_available_bracket_lines.delete(random_available_bracket_line)
else
random_available_bracket_line = available_bracket_lines.sample
wrestler.bracket_line = random_available_bracket_line
available_bracket_lines.delete(random_available_bracket_line)
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

View File

@@ -1,179 +1,161 @@
class WrestlingdevImporter
def initialize( tournament, import_text )
@tournament = tournament
@import_text = import_text
@attribute_separator = ";;"
@model_separator = ",,"
##### Note, the json contains id's for each row in the tables as well as its associations
##### this ignores those ids and uses this tournament id and then looks up associations based on name
##### and this tournament id
attr_accessor :import_data
# Support both parameter styles for backward compatibility
# Old: initialize(tournament, backup)
# New: initialize(tournament) with import_data setter
def initialize(tournament, backup = nil)
@tournament = tournament
# Handle the old style where backup was passed directly
if backup.present?
@import_data = JSON.parse(Base64.decode64(backup.backup_data)) rescue nil
end
end
def import
if Rails.env.production?
self.delay(:job_owner_id => @tournament.id, :job_owner_type => "Importing a backup").import_raw
else
import_raw
end
# Use perform_later which will execute based on centralized adapter config
WrestlingdevImportJob.perform_later(@tournament, @import_data)
end
def import_raw
@tournament.curently_generating_matches = 1
@tournament.save
destroy_all
parse_text
parse_data
@tournament.curently_generating_matches = nil
@tournament.save
end
def destroy_all
@tournament.mats.reload.each do | mat |
mat.destroy
end
@tournament.matches.each do | match |
match.destroy
end
@tournament.schools.each do | school |
school.wrestlers.each do | wrestler |
wrestler.destroy
end
school.destroy
end
@tournament.weights.each do | weight |
weight.destroy
end
# These depend directly on @tournament and will cascade deletes
# due to `dependent: :destroy` in the Tournament model
@tournament.schools.destroy_all # Cascades to Wrestlers, Teampointadjusts, SchoolDelegates
@tournament.weights.destroy_all # Cascades to Wrestlers, Matches
@tournament.mats.destroy_all # Cascades to Matches, MatAssignmentRules
# Explicitly destroy matches again just in case some aren't linked via mats/weights? Unlikely but safe.
# Also handles matches linked directly to tournament if that's possible.
@tournament.matches.destroy_all
@tournament.mat_assignment_rules.destroy_all # Explicitly destroy rules (might be redundant if Mat cascades)
@tournament.delegates.destroy_all
@tournament.tournament_backups.destroy_all
@tournament.tournament_job_statuses.destroy_all
# Note: Teampointadjusts are deleted via School/Wrestler cascade
end
def parse_text
@import_text.each_line do |line|
line_array = line.split(@model_separator,-1)
if line_array[0] == "--Tournament--"
line_array.shift
parse_tournament(line_array)
elsif line_array[0] == "--Schools--"
line_array.shift
parse_schools(line_array)
elsif line_array[0] == "--Weights--"
line_array.shift
parse_weights(line_array)
elsif line_array[0] == "--Mats--"
line_array.shift
parse_mats(line_array)
elsif line_array[0] == "--Wrestlers--"
line_array.shift
parse_wrestlers(line_array)
elsif line_array[0] == "--Matches--"
line_array.shift
parse_matches(line_array)
end
end
def parse_data
parse_tournament(@import_data["tournament"]["attributes"])
parse_schools(@import_data["tournament"]["schools"])
parse_weights(@import_data["tournament"]["weights"])
parse_mats(@import_data["tournament"]["mats"])
parse_wrestlers(@import_data["tournament"]["wrestlers"])
parse_matches(@import_data["tournament"]["matches"])
parse_mat_assignment_rules(@import_data["tournament"]["mat_assignment_rules"])
end
def parse_tournament(tournament_attributes)
tournament_array = tournament_attributes[0].split(@attribute_separator,-1)
@tournament.name = tournament_array[0]
@tournament.address = tournament_array[1]
@tournament.director = tournament_array[2]
@tournament.director_email = tournament_array[3]
@tournament.tournament_type = tournament_array[4]
@tournament.weigh_in_ref = tournament_array[5]
@tournament.user_id = tournament_array[6]
#@tournament.curently_generating_matches = tournament_array[7]
@tournament.date = tournament_array[8]
@tournament.save
end
def parse_wrestlers(wrestlers)
wrestlers.each do | wrestler |
wrestler_array = wrestler.split(@attribute_separator,-1)
new_wrestler = Wrestler.new
new_wrestler.name = wrestler_array[0]
school_id = School.where("name = ? and tournament_id = ?",wrestler_array[1],@tournament.id).first.id
weight_id = Weight.where("max = ? and tournament_id = ?",wrestler_array[2],@tournament.id).first.id
new_wrestler.school_id = school_id
new_wrestler.weight_id = weight_id
new_wrestler.bracket_line = wrestler_array[3]
new_wrestler.original_seed = wrestler_array[4]
new_wrestler.season_win = wrestler_array[5]
new_wrestler.season_loss = wrestler_array[6]
new_wrestler.criteria = wrestler_array[7]
new_wrestler.extra = wrestler_array[8]
new_wrestler.offical_weight = wrestler_array[9]
new_wrestler.pool = wrestler_array[10]
new_wrestler.pool_placement = wrestler_array[11]
new_wrestler.pool_placement_tiebreaker = wrestler_array[12]
new_wrestler.save
end
end
def parse_matches(matches)
matches.each do | match |
@tournament.reload
@tournament.mats.reload
match_array = match.split(@attribute_separator,-1)
new_match = Match.new
weight_id = Weight.where("max = ? and tournament_id = ?",match_array[10],@tournament.id).first.id
if match_array[0].size > 0
w1_id = Wrestler.where("name = ? and weight_id = ?",match_array[0],weight_id).first.id
end
if match_array[1].size > 0
w2_id = Wrestler.where("name = ? and weight_id = ?",match_array[1],weight_id).first.id
end
if match_array[4].size > 0
winner_id = Wrestler.where("name = ? and weight_id = ?",match_array[4],weight_id).first.id
end
if match_array[15].size > 0
# mat_id = Mat.where("name = ? and tournament_id = ?",match_array[15],@tournament.id).first.id
end
new_match.w1 = w1_id if match_array[0].size > 0
new_match.w2 = w2_id if match_array[1].size > 0
new_match.winner_id = winner_id if match_array[4].size > 0
new_match.w1_stat = match_array[2]
new_match.w2_stat = match_array[3]
new_match.win_type = match_array[5]
new_match.score = match_array[6]
new_match.tournament_id = @tournament.id
new_match.round = match_array[7]
new_match. finished = match_array[8]
new_match.bout_number = match_array[9]
new_match.weight_id = weight_id
new_match.bracket_position = match_array[11]
new_match.bracket_position_number = match_array[12]
new_match.loser1_name = match_array[13]
new_match.loser2_name = match_array[14]
# new_match.mat_id = mat_id if match_array[15].size > 0
new_match.save
end
def parse_tournament(attributes)
attributes.except!("id")
@tournament.update(attributes)
end
def parse_schools(schools)
schools.each do | school |
school_array = school.split(@attribute_separator,-1)
new_school = School.new
new_school.tournament_id = @tournament.id
new_school.name = school_array[0]
new_school.score = school_array[1]
new_school.save
schools.each do |school_attributes|
school_attributes.except!("id")
School.create(school_attributes.merge(tournament_id: @tournament.id))
end
end
def parse_weights(weights)
weights.each do | weight |
weight_array = weight.split(@attribute_separator,-1)
new_weight = Weight.new
new_weight.tournament_id = @tournament.id
new_weight.max = weight_array[0]
new_weight.save
weights.each do |weight_attributes|
weight_attributes.except!("id")
Weight.create(weight_attributes.merge(tournament_id: @tournament.id))
end
end
def parse_mats(mats)
mats.each do | mat |
mat_array = mat.split(@attribute_separator,-1)
new_mat = Mat.new
new_mat.tournament_id = @tournament.id
new_mat.name = mat_array[0]
new_mat.save
mats.each do |mat_attributes|
mat_attributes.except!("id")
Mat.create(mat_attributes.merge(tournament_id: @tournament.id))
end
end
end
def parse_mat_assignment_rules(mat_assignment_rules)
mat_assignment_rules.each do |rule_attributes|
mat_name = rule_attributes.dig("mat", "name")
mat = Mat.find_by(name: mat_name, tournament_id: @tournament.id)
# Prefer the new "weight_class_maxes" key emitted by backups (human-readable
# max values). If not present, fall back to the legacy "weight_classes"
# value which may be a comma-separated string or an array of IDs.
if rule_attributes.key?("weight_class_maxes") && rule_attributes["weight_class_maxes"].respond_to?(:map)
new_weight_classes = rule_attributes["weight_class_maxes"].map do |max_value|
Weight.find_by(max: max_value, tournament_id: @tournament.id)&.id
end.compact
elsif rule_attributes["weight_classes"].is_a?(Array)
# Already an array of IDs
new_weight_classes = rule_attributes["weight_classes"].map(&:to_i)
elsif rule_attributes["weight_classes"].is_a?(String)
# Comma-separated IDs stored in the DB column; split into integers.
new_weight_classes = rule_attributes["weight_classes"].to_s.split(",").map(&:strip).reject(&:empty?).map(&:to_i)
else
new_weight_classes = []
end
# Extract bracket_positions and rounds (leave as-is; model will coerce if needed)
bracket_positions = rule_attributes["bracket_positions"]
rounds = rule_attributes["rounds"]
# Remove any keys we don't want to mass-assign (including both old/new weight keys)
rule_attributes.except!("id", "mat", "tournament_id", "weight_classes", "weight_class_maxes")
MatAssignmentRule.create(
rule_attributes.merge(
tournament_id: @tournament.id,
mat_id: mat&.id,
weight_classes: new_weight_classes,
bracket_positions: bracket_positions,
rounds: rounds
)
)
end
end
def parse_wrestlers(wrestlers)
wrestlers.each do |wrestler_attributes|
school = School.find_by(name: wrestler_attributes["school"]["name"], tournament_id: @tournament.id)
weight = Weight.find_by(max: wrestler_attributes["weight"]["max"], tournament_id: @tournament.id)
wrestler_attributes.except!("id", "school", "weight")
Wrestler.create(wrestler_attributes.merge(
school_id: school&.id,
weight_id: weight&.id
))
end
end
def parse_matches(matches)
matches.each do |match_attributes|
next unless match_attributes # Skip if match_attributes is nil
weight = Weight.find_by(max: match_attributes.dig("weight", "max"), tournament_id: @tournament.id)
mat = Mat.find_by(name: match_attributes.dig("mat", "name"), tournament_id: @tournament.id)
w1 = Wrestler.find_by(name: match_attributes["w1_name"], weight_id: weight&.id) if match_attributes["w1_name"]
w2 = Wrestler.find_by(name: match_attributes["w2_name"], weight_id: weight&.id) if match_attributes["w2_name"]
winner = Wrestler.find_by(name: match_attributes["winner_name"], weight_id: weight&.id) if match_attributes["winner_name"]
match_attributes.except!("id", "weight", "mat", "w1_name", "w2_name", "winner_name", "tournament_id")
Match.create(match_attributes.merge(
tournament_id: @tournament.id,
weight_id: weight&.id,
mat_id: mat&.id,
w1: w1&.id,
w2: w2&.id,
winner_id: winner&.id
))
end
end
end

View File

@@ -5,111 +5,35 @@ class GeneratePoolNumbers
def savePoolNumbers
@weight.wrestlers.each do |wrestler|
if wrestler.pool and (wrestler.pool) > (@weight.pools)
resetPool
elsif @weight.one_pool_empty
resetPool
end
end
if @weight.pools == 4
saveFourPoolNumbers(@weight.wrestlers_without_pool_assignment)
elsif @weight.pools == 2
saveTwoPoolNumbers(@weight.wrestlers_without_pool_assignment)
elsif @weight.pools == 1
saveOnePoolNumbers(@weight.wrestlers_without_pool_assignment)
elsif @weight.pools == 8
saveEightPoolNumbers(@weight.wrestlers_without_pool_assignment)
end
saveRandomPool(@weight.reload.wrestlers_without_pool_assignment)
end
def resetPool
@weight.wrestlers.each do |wrestler|
wrestler.pool = nil
wrestler.save
@weight.reload
end
end
def saveRandomPool(poolWrestlers)
pool = 1
poolWrestlers.sort_by{|x| x.bracket_line }.reverse.each do |wrestler|
wrestler.pool = pool
wrestler.save
if pool < @weight.pools
pool = pool + 1
else
pool = 1
end
end
end
def saveOnePoolNumbers(poolWrestlers)
poolWrestlers.sort_by{|x| x.bracket_line }.each do |w|
w.pool = 1
w.save
wrestler.pool = get_wrestler_pool_number(@weight.pools, wrestler.bracket_line)
wrestler.save
end
end
def saveTwoPoolNumbers(poolWrestlers)
poolWrestlers.sort_by{|x| x.bracket_line }.reverse.each do |w|
if w.bracket_line == 1
w.pool = 1
elsif w.bracket_line == 2
w.pool = 2
elsif w.bracket_line == 3
w.pool = 2
elsif w.bracket_line == 4
w.pool = 1
end
w.save
def get_wrestler_pool_number(number_of_pools, wrestler_seed)
# Convert seed to zero-based index for easier math
zero_based = wrestler_seed - 1
# Determine which "row" we're on (0-based)
# e.g., with 4 pools:
# seeds 1..4 are in row 0,
# seeds 5..8 are in row 1,
# seeds 9..12 in row 2, etc.
row = zero_based / number_of_pools
# Column within that row (also 0-based)
col = zero_based % number_of_pools
if row.even?
# Row is even => left-to-right
# col=0 => pool 1, col=1 => pool 2, etc.
pool = col + 1
else
# Row is odd => right-to-left
# col=0 => pool number_of_pools, col=1 => pool number_of_pools-1, etc.
pool = number_of_pools - col
end
end
def saveFourPoolNumbers(poolWrestlers)
poolWrestlers.sort_by{|x| x.bracket_line }.reverse.each do |w|
if w.bracket_line == 1
w.pool = 1
elsif w.bracket_line == 2
w.pool = 2
elsif w.bracket_line == 3
w.pool = 3
elsif w.bracket_line == 4
w.pool = 4
elsif w.bracket_line == 8
w.pool = 1
elsif w.bracket_line == 7
w.pool = 2
elsif w.bracket_line == 6
w.pool = 3
elsif w.bracket_line == 5
w.pool = 4
end
w.save
end
end
def saveEightPoolNumbers(poolWrestlers)
poolWrestlers.sort_by{|x| x.bracket_line }.reverse.each do |w|
if w.bracket_line == 1
w.pool = 1
elsif w.bracket_line == 2
w.pool = 2
elsif w.bracket_line == 3
w.pool = 3
elsif w.bracket_line == 4
w.pool = 4
elsif w.bracket_line == 5
w.pool = 5
elsif w.bracket_line == 6
w.pool = 6
elsif w.bracket_line == 7
w.pool = 7
elsif w.bracket_line == 8
w.pool = 8
end
w.save
end
end
pool
end
end

View File

@@ -5,7 +5,7 @@ class CalculateWrestlerTeamScore
end
def totalScore
if @wrestler.extra or @wrestler.matches.count == 0
if @wrestler.extra or @wrestler.all_matches.count == 0
return 0
else
earnedPoints - deductedPoints
@@ -26,8 +26,8 @@ class CalculateWrestlerTeamScore
def placement_points
return PoolBracketPlacementPoints.new(@wrestler).calcPoints if @tournament.tournament_type == "Pool to bracket"
return ModifiedSixteenManPlacementPoints.new(@wrestler).calc_points if @tournament.tournament_type == "Modified 16 Man Double Elimination"
return DoubleEliminationPlacementPoints.new(@wrestler).calc_points if @tournament.tournament_type == "Double Elimination 1-6"
return ModifiedSixteenManPlacementPoints.new(@wrestler).calc_points if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
return DoubleEliminationPlacementPoints.new(@wrestler).calc_points if @tournament.tournament_type.include? "Regular Double Elimination"
return 0
end
@@ -43,16 +43,43 @@ class CalculateWrestlerTeamScore
end
end
def pool_bonus_points
if @tournament.tournament_type == "Pool to bracket"
(@wrestler.pin_wins.select{|m| m.bracket_position == "Pool"}.size * 2) + (@wrestler.tech_wins.select{|m| m.bracket_position == "Pool"}.size * 1.5) + (@wrestler.major_wins.select{|m| m.bracket_position == "Pool"}.size * 1)
else
0
end
end
def byePoints
points = 0
if @tournament.tournament_type == "Pool to bracket"
if @wrestler.pool_wins.size >= 1 and @wrestler.has_a_pool_bye == true
2
else
0
points += 2
end
else
0
end
if @tournament.tournament_type.include? "Regular Double Elimination"
if @wrestler.championship_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "1/2" and m.win_type != "BYE"}.size > 0
# if they have a win in the championship round or if they got a bye all the way to finals and won the finals
points += @wrestler.championship_byes.size * 2
end
if @wrestler.consolation_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "3/4" and m.win_type != "BYE"}.size > 0
# if they have a win in the consolation round or if they got a bye all the way to 3rd/4th match and won
points += @wrestler.consolation_byes.size * 1
end
end
if @tournament.tournament_type.include? "Modified 16 Man Double Elimination"
if @wrestler.championship_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "1/2" and m.win_type != "BYE"}.size > 0
# if they have a win in the championship round or if they got a bye all the way to finals and won the finals
points += @wrestler.championship_byes.size * 2
end
if @wrestler.consolation_advancement_wins.size > 0 or @wrestler.matches_won.select{|m| m.bracket_position == "5/6" and m.win_type != "BYE"}.size > 0
# if they have a win in the consolation round or if they got a bye all the way to 5th/6th match and won
# since the consolation bracket goes to 5/6 in a modified tournament
points += @wrestler.consolation_byes.size * 1
end
end
return points
end
def bonusWinPoints

View File

@@ -1,9 +1,7 @@
class DoubleEliminationPlacementPoints
def initialize(wrestler)
@wrestler = wrestler
if wrestler.tournament.tournament_type == "Double Elimination 1-6"
@number_of_placers = 6
end
@number_of_placers = @wrestler.tournament.number_of_placers
end
def calc_points
@@ -22,17 +20,29 @@ class DoubleEliminationPlacementPoints
elsif won_bracket_position_size("7/8") > 0
return PlacementPoints.new(@number_of_placers).seventhPlace
elsif bracket_position_size("Conso Quarter") > 0 and @number_of_placers >= 8
return PlacementPoints.new(@number_of_placers).eigthPlace
return PlacementPoints.new(@number_of_placers).eighthPlace
else
return 0
end
end
def bracket_position_size(bracket_position_name)
@wrestler.all_matches.select{|m| m.bracket_position == bracket_position_name}.size
@wrestler.all_matches.select{|m| m.bracket_position == bracket_position_name}.size
end
def won_bracket_position_size(bracket_position_name)
@wrestler.matches_won.select{|m| m.bracket_position == bracket_position_name}.size
end
def bracket_placement_points(bracket_position_name)
if bracket_position_name == "Did not place"
return 0
end
if @wrestler.participating_matches.where(bracket_position: bracket_position_name).count > 0
points = Teampointadjust.find_by(tournament_id: @wrestler.tournament.id, points_for_placement: bracket_position_name)
if points
# ... existing code ...
end
end
end
end

View File

@@ -1,7 +1,8 @@
class ModifiedSixteenManPlacementPoints
def initialize(wrestler)
@wrestler = wrestler
@number_of_placers = 6
@wrestler = wrestler
@number_of_placers = @wrestler.tournament.number_of_placers
end
def calc_points
@@ -17,6 +18,10 @@ class ModifiedSixteenManPlacementPoints
return PlacementPoints.new(@number_of_placers).fifthPlace
elsif bracket_position_size("5/6") > 0
return PlacementPoints.new(@number_of_placers).sixthPlace
elsif won_bracket_position_size("7/8") > 0
return PlacementPoints.new(@number_of_placers).seventhPlace
elsif bracket_position_size("Conso Semis") > 0 and @number_of_placers >= 8
return PlacementPoints.new(@number_of_placers).eighthPlace
else
return 0
end

View File

@@ -3,13 +3,13 @@ json.cache! ["api_tournament", @tournament] do
json.content(@tournament)
json.(@tournament, :id, :name, :address, :director, :director_email, :tournament_type, :created_at, :updated_at, :user_id)
json.schools @tournament.schools do |school|
json.schools @schools do |school|
json.id school.id
json.name school.name
json.score school.score
end
json.weights @tournament.weights do |weight|
json.weights @weights do |weight|
json.id weight.id
json.max weight.max
json.bracket_size weight.bracket_size
@@ -26,7 +26,7 @@ json.cache! ["api_tournament", @tournament] do
end
end
json.mats @tournament.mats do |mat|
json.mats @mats do |mat|
json.name mat.name
json.unfinished_matches mat.unfinished_matches do |match|
json.bout_number match.bout_number
@@ -35,7 +35,7 @@ json.cache! ["api_tournament", @tournament] do
end
end
json.unassignedMatches @tournament.matches.select{|m| m.mat_id == nil}.sort_by{|m| m.bout_number}[0...9] do |match|
json.unassignedMatches @matches.select{|m| m.mat_id == nil}.sort_by{|m| m.bout_number}[0...9] do |match|
json.bout_number match.bout_number
json.w1_name match.w1_name
json.w2_name match.w2_name
@@ -43,7 +43,7 @@ json.cache! ["api_tournament", @tournament] do
json.round match.round
end
json.matches @tournament.matches do |match|
json.matches @matches do |match|
json.bout_number match.bout_number
json.w1_name match.w1_name
json.w2_name match.w2_name

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