Compare commits

..

321 Commits

Author SHA1 Message Date
Matt Young 80d7bc3ebe Fix issue with year end resets. 2025-12-13 14:52:11 -06:00
Matt Young 38d7826218 catch exception on judging controller 2025-12-13 11:04:56 -06:00
Matt Young 59629e227d correct missing template info 2025-12-08 20:24:07 -06:00
Matt Young 755f8bdf4a correct testing issue. Monitor controller needs proper tests written. 2025-12-08 15:58:24 -06:00
Matt Young bf502f4cbb fix duplicate route name 2025-12-08 15:49:44 -06:00
Matt Young 2ffe14e43c fix duplicate route nqame 2025-12-08 11:30:18 -06:00
Matt Young d55be47f41 Update logging config. 2025-12-08 10:56:41 -06:00
Matt Young 1c3bb39805 Add console command to force recalculation of judge totals 2025-11-20 11:21:17 -06:00
Matt Young 55d5dba840 Fix error where a modified subscore would not count for seating if there is no advancement. 2025-11-20 10:37:45 -06:00
Matt Young a5b203af2e Allow printing of blank sheet of cards 2025-11-16 15:29:33 -06:00
Matt Young 5bbcccdc22 update vite config 2025-11-16 15:23:42 -06:00
Matt Young a9551a1dd6 update packages 2025-11-09 16:34:32 -06:00
Matt Young 7d94ee2cfb add page listing school email domains. 2025-11-09 15:47:08 -06:00
Matt Young 87e3ec322d Quickfix dealing with a student incorrectly identifying as a doubler when not entered for seating in one audition but only advancement 2025-11-08 17:16:21 -06:00
Matt Young 6f657415aa Fix issue with seating including advance only entries. 2025-11-08 15:55:26 -06:00
Matt Young 67ceae6f01 Fix issue in advancement - ignore seating only 2025-11-08 13:43:08 -06:00
Matt Young a59217db41 Fix error in card printing 2025-11-06 17:22:40 -06:00
Matt Young 402cbf8c83 Show what a student is auditioning for on their cards if advancement audition 2025-11-06 07:30:09 -06:00
Matt Young be621606e2 Show doublers on cards. 2025-11-05 06:55:12 -06:00
Matt Young 59c5ae8526 Show doublers on sign in sheets. 2025-11-05 06:48:52 -06:00
Matt Young 7347059d96 only bill for students entered for seating. 2025-11-05 06:40:39 -06:00
Matt Young 834de902ac Update results on directors page to add explaination. 2025-11-03 09:02:41 -06:00
Matt Young 165d2c9f6c Fixed issue with results output 2025-11-03 07:13:59 -06:00
Matt Young 3208b31524 Undo prelim score on results page 2025-11-01 15:41:59 -05:00
Matt Young b8a4cf5f39 Undo prelim score on results page 2025-11-01 13:44:46 -05:00
Matt Young 67622ec0c9 Undo prelim score on results page 2025-11-01 12:50:37 -05:00
Matt Young fb77923812 Show prelim score on seating page. 2025-11-01 12:45:02 -05:00
Matt Young bfee058078 Show prelim score on seating page. 2025-11-01 12:41:53 -05:00
Matt Young 10a4d1a140 Add abilitly for admin to manually set password for users 2025-10-28 11:35:30 -05:00
Matt Young 2dfb745861 Add artisan commands to import entries from a CSV file 2025-10-27 22:59:54 -05:00
Matt Young 3315efc83b Allow bulk updating of auditions
Closes #31
2025-10-27 17:31:18 -05:00
Matt Young 4548be098a Allow admins to rename events.
Closes #114
2025-10-26 17:46:55 -05:00
Matt Young 1acb286ac8 Allow the modification of Bonus Score definition data.
Closes #100
Closes #58
2025-10-25 22:53:21 -05:00
Matt Young 6ca05bf4d5 Allow admin to set users to have no school.
Fixes #112
2025-10-25 22:21:10 -05:00
Matt Young 550614a317 Show an error when an administrator attempts to duplicate an existing entry. 2025-10-25 22:06:09 -05:00
Matt Young a5f11fb897 Fix error in doubler request page. 2025-10-24 08:32:48 -05:00
Matt Young 69be2b7ed0 Fix error in doubler request page. 2025-10-23 09:09:15 -05:00
Matt Young 956d70a90e Correct issue where a subscore could not be created for seating only. 2025-10-22 19:21:35 -05:00
Matt Young 0ca239d297 Merge branch 'refs/heads/pass_fail_prelims' 2025-10-20 22:33:34 -05:00
Matt Young aa967c317b Update fictionalize command 2025-10-20 12:51:52 -05:00
Matt Young 1af9715682 Show related log entries on admin pages. 2025-10-20 01:49:06 -05:00
Matt Young 0307fbc595 Block entry of a prelim score for an entry with any finals scores. 2025-10-20 01:11:04 -05:00
Matt Young 30cbaf69f8 Allow admin and tabulators to enter and modify prelim scores. 2025-10-20 01:01:27 -05:00
Matt Young 62a3694c03 Enhancement to monitor pages. 2025-10-19 08:36:37 -05:00
Matt Young 3fb3f8b3df Enhancement to monitor pages. 2025-10-19 08:20:34 -05:00
Matt Young 1041d7c96b Enhancement to monitor pages. 2025-10-19 08:14:01 -05:00
Matt Young ee958d350d Enhancement to monitor pages. 2025-10-18 22:31:53 -05:00
Matt Young 40363a5964 Allow editing of scores by prelims judges. 2025-10-18 15:04:12 -05:00
Matt Young b978966c98 Minimize information shown on monitor screen. Allow monitors to enter no-shows for prelim auditions. 2025-10-18 14:04:04 -05:00
Matt Young 3e3b99c56c Update look of monitor page 2025-10-16 14:39:54 -05:00
Matt Young 70f79d031c Fix issue where a scoring guide subscore could not be created if advancement was not enabled. 2025-10-16 11:53:15 -05:00
Matt Young 31d56e5b90 Show failed prelim scores on seating page 2025-10-15 14:02:41 -05:00
Matt Young b2d66eb1b8 If an audition has a prelim, only show finals judges entries that have passed prelims. 2025-10-14 18:20:09 -05:00
Matt Young 982dfa46a0 Rehash monitor page to deal with prelims 2025-10-14 17:35:14 -05:00
Matt Young add9f9e25d Check for a prelim result after entering a prelim score. 2025-10-14 09:12:14 -05:00
Matt Young 761f63aa55 Added CheckPrelimResult action to check if an entry passed it's prelim audition and make the appropriate flag on the entry. 2025-10-13 22:13:11 -05:00
Matt Young ccd206c2af remove depricasted code 2025-10-11 20:36:58 -05:00
Matt Young 011900461a Show previously entered prelim scores on entry list. 2025-10-09 18:58:26 -05:00
Matt Young 0e4b8acce6 Judge prelim entry scores functioning. 2025-10-08 21:50:02 -05:00
Matt Young ca80260bda Action to enter prelim score sheet implemented. 2025-10-08 07:32:59 -05:00
Matt Young 83eff8feee Preliminary work on PrelimJudging entry list. 2025-10-07 21:11:26 -05:00
Matt Young 07f3f37be4 Merge branch 'master' into pass_fail_prelims
* master:
  Correct issue with testing.
2025-10-02 21:57:55 -05:00
Matt Young 87046bb736 Correct issue with testing. 2025-10-02 21:57:31 -05:00
Matt Young 3b6fbc16f1 Merge branch 'master' into pass_fail_prelims
* master:
  Fix issue where entry observer was taking too long during draw. No need to update doublers just to run the draw.
  Fix issue where directors could add students down to first grade if no nomination ensembles were defined.
2025-10-02 21:06:23 -05:00
Matt Young 8a2b2256cf Fix issue where entry observer was taking too long during draw. No need to update doublers just to run the draw. 2025-10-02 21:02:27 -05:00
Matt Young 14b275aa7e Work on auth for PrelimJudging 2025-10-02 20:43:22 -05:00
Matt Young 88608ea5b4 Test for prelim auditions showing on judging dashboard. 2025-09-22 22:13:48 -05:00
Matt Young 49b203cc25 Test for prelim auditions showing on judging dashboard. 2025-09-22 21:21:32 -05:00
Matt Young 23442ad740 prelim score sheet model and migration 2025-09-22 21:15:32 -05:00
Matt Young 2b39ea9a88 Setup PrelimJudgingController.php 2025-09-22 21:05:10 -05:00
Matt Young a609c9d627 Show assigned prelim auditions on judging dashboard 2025-09-22 20:59:32 -05:00
Matt Young 2418873af0 Ease the process of assigning prelim auditions to rooms. 2025-09-22 19:27:18 -05:00
Matt Young 81b10220d6 Set up logging for PrelimDefinitions 2025-09-14 21:19:41 -05:00
Matt Young 674374b6b6 Add ability to delete prelim auditions. Need to add an observer to log. 2025-09-13 22:59:04 -05:00
Matt Young cafa1ddf29 Add prelim auditions to the menu. 2025-09-13 22:17:38 -05:00
Matt Young b7b5d0fc94 Most basic management function of prelim definitions done. Need to add delete method and listener for logging next. 2025-09-11 23:02:37 -05:00
Matt Young 8f41af74f9 Fix issue where directors could add students down to first grade if no nomination ensembles were defined. 2025-09-11 17:59:37 -05:00
Matt Young 352897fa25 development on management of prelims entries 2025-09-11 16:53:03 -05:00
Matt Young 7c0504ea89 prelim definition relationship tests. 2025-09-11 10:04:11 -05:00
Matt Young a7d1776c44 Create model and migration for prelim definitions 2025-09-11 09:25:33 -05:00
Matt Young 1953eedb0b Implement OneFeePerStudentPerEvent option 2025-09-10 21:29:16 -05:00
Matt Young 340fae6747 Create InvoiceOneFeePerStudentPerEvent implementation of InvoiceDataService and set up ability to select and use it. 2025-09-10 21:20:22 -05:00
Matt Young aa92d66ff4 Update one fee per entry - don't charge if an entry is for advancement only. 2025-09-10 18:44:43 -05:00
Matt Young 4f46328026 On the admin entry page, display entry timestamps in central time. 2025-09-09 19:38:50 -05:00
Matt Young 8cfb9c1f06 If a user chooses a school, and they're the only director at that school, they'll be made head. 2025-08-18 20:07:40 -05:00
Matt Young 5f360c7d3a Correct issue where users were unable to choose their school. 2025-08-17 10:03:52 -05:00
Matt Young 3dc5ae1793 Fix issue where service providers prevented initial database migration 2025-08-05 15:47:32 -05:00
Matt Young df48386818 Fix issue where service providers prevented initial database migration 2025-08-05 15:38:10 -05:00
Matt Young 437ba6020b Comments on MakeSeatingDecisionsController.php and add test for that file 2025-08-05 07:48:39 -05:00
Matt Young 09f4ed6636 Tets for EnterDoublerDecisionController 2025-07-16 08:27:08 -05:00
Matt Young d09d053b6a Tets for BonusScoreController 2025-07-15 11:13:33 -05:00
Matt Young 119b8228c3 Tets for BonusScoreController 2025-07-15 10:18:43 -05:00
Matt Young 0af15bf753 Tets for BonusScoreController 2025-07-15 09:56:34 -05:00
Matt Young 27e68ab233 Tets for BonusScoreController 2025-07-14 23:36:41 -05:00
Matt Young 5e9c7a5084 Removed unused DoublerDecision 2025-07-14 14:24:44 -05:00
Matt Young 1cc43c1bce Removed unused EntryScoreController 2025-07-14 13:28:36 -05:00
Matt Young 8370810c03 Removed unused SeatingPublicationController 2025-07-12 22:37:52 -05:00
Matt Young 8ad74947af Refactor auditionseating controller and related views into multiples files for easier management 2025-07-12 11:08:07 -05:00
Matt Young 56f302ef10 Tests for SeatingStatusController 2025-07-10 17:30:38 -05:00
Matt Young 3b8c7e6d12 Tests for JudgingController.php 2025-07-10 15:30:45 -05:00
Matt Young 118a465bb7 Tests for BonusScoreRecordController.php 2025-07-10 04:08:53 -05:00
Matt Young f946c157ab Tests for BonusScoreEntryListController.php 2025-07-10 03:39:56 -05:00
Matt Young 21c2af9172 Tests for BonusScoreEntryController.php 2025-07-10 03:24:48 -05:00
Matt Young 5014e80fb1 Tests for Audition controller 2025-07-10 02:52:02 -05:00
Matt Young eb66da14cf Tests for Audition controller 2025-07-10 00:33:38 -05:00
Matt Young 185c91e717 Tests for AuditionSettings controller 2025-07-09 23:01:58 -05:00
Matt Young aeb2557be3 Tests for BonusScoreDefinitionController.php 2025-07-09 22:49:09 -05:00
Matt Young beae7cae20 Work on deprecating DrawService 2025-07-09 16:14:57 -05:00
Matt Young 74b9d3f141 add test for admin DrawController. Work on deprecating DrawService 2025-07-09 16:10:56 -05:00
Matt Young 7efe029ff9 add test for admin DrawController. Work on deprecating DrawService 2025-07-09 15:51:42 -05:00
Matt Young e1d72ee040 add test for admin EnsembleController 2025-07-09 02:33:41 -05:00
Matt Young fa25e76c5b add test for admin EntryController 2025-07-08 23:07:28 -05:00
Matt Young 4963124d22 add testValidity method to ScoreSheet model. Aimed to replace scoreSheetService::isScoreSheetValid 2025-07-08 21:36:36 -05:00
Matt Young b09f1b13ca updates to entry action 2025-07-08 16:27:12 -05:00
Matt Young 66fe859f06 Tets for Admin EventController 2025-07-08 10:48:52 -05:00
Matt Young 47200b1f01 Putting off testing printing. 2025-07-08 10:32:13 -05:00
Matt Young 4706d0ac4d LogViewer test 2025-07-08 10:15:24 -05:00
Matt Young 7986fa0679 ignore coverage measurement for printing related controllers 2025-07-08 02:30:21 -05:00
Matt Young 4358201909 Add tests for admin/RoomContoller 2025-07-08 02:30:00 -05:00
Matt Young e4a646a4ce Add tests for admin/SchoolController 2025-07-07 23:56:52 -05:00
Matt Young 2e8d625ab0 Add Scoring Guide controller test 2025-07-07 21:52:10 -05:00
Matt Young 36da3b0e78 Add Scoring Guide observer 2025-07-07 14:39:47 -05:00
Matt Young 68bdd9f30f Progress on student controller rewrite and testing. 2025-07-07 13:34:45 -05:00
Matt Young c058b92930 Progress on student controller rewrite and testing. 2025-07-07 02:02:18 -05:00
Matt Young d1985f4a57 Dashboard Controller testing 2025-07-07 00:10:13 -05:00
Matt Young 8210f7f6a0 Dashboard Controller testing 2025-07-07 00:09:36 -05:00
Matt Young 099f36b48c Dashboard Controller testing 2025-07-06 01:20:55 -05:00
Matt Young 45bf624bfc Error resolution 2025-07-06 00:52:13 -05:00
Matt Young 96b590365a Testing for DoublerRequestController.php 2025-07-05 23:44:16 -05:00
Matt Young ba55d75172 add some observers for logging purposes 2025-07-05 23:43:42 -05:00
Matt Young d21e568d60 add forSchool method to UserFactory 2025-07-05 21:57:43 -05:00
Matt Young bb4785fdbf Add doublers relationship to School model 2025-07-05 21:23:47 -05:00
Matt Young 30667a28c5 Add doublerRequest relationship to School model 2025-07-05 21:18:52 -05:00
Matt Young dcd1d74fdc Refactor and test EntryController 2025-07-05 19:46:45 -05:00
Matt Young f3dd8bea0d Refactor and test FilterController 2025-07-05 15:22:18 -05:00
Matt Young a56e908f8c Refactor and test MonitorController 2025-07-05 13:27:58 -05:00
Matt Young 0ea7ea2f14 Results page testing and disregarding coverage measurement for some classes 2025-07-05 12:02:45 -05:00
Matt Young d9688fd3b0 School controller refactoring and testing. 2025-07-05 02:54:27 -05:00
Matt Young f3b2372682 School controller refactoring and testing. 2025-07-05 01:11:26 -05:00
Matt Young 2962854541 Student controller refactoring and testing. 2025-07-04 23:52:25 -05:00
Matt Young 7379500e9a Make CreateStudent action and test. 2025-07-04 19:43:37 -05:00
Matt Young 9717ae852e Work on refactoring student controller and test 2025-07-04 17:20:49 -05:00
Matt Young c22f3ddadf Code coverage ignore
Consider removing UserController
2025-07-04 15:23:21 -05:00
Matt Young db34a86545 Ignore classes for code coverage 2025-07-04 14:47:14 -05:00
Matt Young 409aa939c3 Created a test for app/Rules/UniqueFullNameAtSchool 2025-07-04 14:41:35 -05:00
Matt Young 07c7a27e28 Ignore providers for coverage 2025-07-04 14:28:54 -05:00
Matt Young 07639ca6d0 Ignore for coverage 2025-07-04 14:15:42 -05:00
Matt Young 8a50c4f523 Remove unneeded declarations 2025-07-04 14:13:48 -05:00
Matt Young f9cb50a445 Create test for app/Observers/ScoreSheetObserver 2025-07-04 14:03:26 -05:00
Matt Young fbe74571f6 Create test for app/Observers/ScoreSheetObserver 2025-07-04 14:02:13 -05:00
Matt Young 4a4947f8bf Create test for app/Observers/SchoolEmailDomainObserver 2025-07-04 13:17:03 -05:00
Matt Young 429b26b3f7 Create test for app/Observers/EntryObserver 2025-07-04 13:08:52 -05:00
Matt Young 86fb7a7e62 replace calls to Doubler model static methods with calls to the DoublerSync action 2025-07-04 12:27:41 -05:00
Matt Young 879403cc33 update entryFlagObserver and it's tests to use the DoublerSync action. 2025-07-04 12:06:31 -05:00
Matt Young 0b54b57e41 create test for app/actions/tabulation/DoublerSync 2025-07-04 11:49:31 -05:00
Matt Young 6715c9346a Create DoublerSync action to replace static methods on Doubler class. 2025-07-04 11:31:43 -05:00
Matt Young 15650f28ad add todo 2025-07-04 11:23:05 -05:00
Matt Young d609b0d39b Create test for app/Observers/EntryFlagObserver 2025-07-04 04:11:37 -05:00
Matt Young 0c3a673bad Create test for app/Observers/BonusScoreObserver 2025-07-04 02:32:32 -05:00
Matt Young f45aaec506 Create test for app/Observers/BonusScoreObserver 2025-07-04 02:31:00 -05:00
Matt Young c7ffe6be02 Remove unused Observers 2025-07-03 23:14:37 -05:00
Matt Young 762ecd487d Create tests for app/Models/UserFlag 2025-07-03 23:08:11 -05:00
Matt Young efe79d6ad5 Create tests for app/Models/User 2025-07-03 22:54:04 -05:00
Matt Young c6b4522f9e Create tests for app/Models/Student and Audition 2025-07-03 21:40:43 -05:00
Matt Young bcbbf92457 Create tests for app/Models/ScoringGuide 2025-07-03 16:31:54 -05:00
Matt Young b9f6bf5917 Create tests for app/Models/ScoreSheet 2025-07-03 16:26:26 -05:00
Matt Young 9b8584777e Create tests for app/Models/SchoolEmailDomain 2025-07-03 16:14:53 -05:00
Matt Young 08fd7a215a Create tests for app/Models/School 2025-07-03 16:07:55 -05:00
Matt Young f831ad6cbd Create tests for app/Models/RoomUser 2025-07-03 16:07:44 -05:00
Matt Young 3abf0e09aa Create tests for app/Models/NominationEnsembleEntry 2025-07-03 15:43:44 -05:00
Matt Young 962a5a109f Create tests for app/Models/NominationEnsembleEntry 2025-07-03 15:14:26 -05:00
Matt Young 1c918cd559 Create tests for app/Models/NominationEnsemble 2025-07-03 15:10:23 -05:00
Matt Young d9bcae5d2a Create tests for app/Models/judgeAdvancementVote 2025-07-03 14:39:56 -05:00
Matt Young 711f2d2859 Create tests for app/Models/judgeAdvancementVote 2025-07-03 14:39:07 -05:00
Matt Young e01ff95432 Create tests for app/Models/HistoricalSeat 2025-07-03 14:34:23 -05:00
Matt Young 50cefdc05d Create tests for app/Models/HistoricalSeat 2025-07-03 14:33:29 -05:00
Matt Young 731a6440b1 Create tests for app/Models/Event 2025-07-03 14:29:19 -05:00
Matt Young f12dd416d7 Create tests for app/Models/Event 2025-07-03 14:29:01 -05:00
Matt Young fe9c3612be Create tests for app/Models/EntryTotalScore 2025-07-03 14:22:17 -05:00
Matt Young 899299fd99 Create tests for app/Models/Entry 2025-07-03 14:17:46 -05:00
Matt Young f37db47d27 Create tests for app/Models/Ensemble 2025-07-03 12:56:01 -05:00
Matt Young 274fd4cbad Create tests for app/Models/DoublerRequest 2025-07-03 10:42:46 -05:00
Matt Young dd0eb75a33 Prepare DoublerEntryCount for deprecation 2025-07-03 10:37:24 -05:00
Matt Young 51b2b01359 Create tests for app/Models/Doubler 2025-07-03 10:26:13 -05:00
Matt Young c1f7d58efb Create tests for app/Models/BonusScore 2025-07-03 03:27:19 -05:00
Matt Young 1466cc8c32 Create tests for app/Models/AuditionFlag 2025-07-03 03:14:33 -05:00
Matt Young 8d76c9e66d Create tests for app/Models/AuditLogEntry 2025-07-03 03:10:44 -05:00
Matt Young c309c23ca6 Create tests for app/Models/Audition 2025-07-03 03:07:45 -05:00
Matt Young 0f1ed33216 Create tests for app/Listeners/LogLogout 2025-07-03 00:31:20 -05:00
Matt Young dc552407c4 Create tests for app/Listeners/LogLogin 2025-07-03 00:27:39 -05:00
Matt Young fad7e1199e Create tests for app/Settings 2025-07-02 23:44:16 -05:00
Matt Young dc8ad3a905 Create tests for app/helpers 2025-07-02 23:28:15 -05:00
Matt Young bd14c10b93 Ignore testing coverage in console commands and exceptions 2025-07-02 23:06:41 -05:00
Matt Young 471fe11570 Create tests for app/actions/YearEndProcedures/YearEndCleanup 2025-07-02 22:56:43 -05:00
Matt Young bc12c90049 Create tests for app/actions/YearEndProcedures/RecordHistoricalSeats 2025-07-02 22:40:18 -05:00
Matt Young 1a3d88bfa8 Create tests for app/actions/tabulation/RankAuditionEntries 2025-07-02 13:33:19 -05:00
Matt Young 157e2f496a Create tests for app/actions/tabulation/UnpublishSeats 2025-07-02 11:52:59 -05:00
Matt Young 1253c18087 Ignore GetAuditionSeats consider deleting this action 2025-07-02 11:52:00 -05:00
Matt Young fbd99c003d Create tests for app/actions/tabulation/PublishSeats 2025-07-02 11:35:13 -05:00
Matt Young ba15191fca Exclude reports and print from testing for now 2025-07-02 11:08:05 -05:00
Matt Young c67c1ad79c Create tests for app/actions/tabulation/TotalEntryScore 2025-07-02 10:29:21 -05:00
Matt Young 53ccc5a7a3 Create tests for app/actions/tabulation/EnterScore 2025-07-02 03:30:55 -05:00
Matt Young a924b3bf51 Create tests for app/actions/tabulation/EnterNoShow 2025-07-02 02:52:43 -05:00
Matt Young 23de496ce6 Create tests for app/actions/fortify/EnterBonusScore 2025-07-02 02:30:01 -05:00
Matt Young 9556e7909a Create tests for app/actions/fortify/CalculateAuditionScores 2025-07-02 00:48:51 -05:00
Matt Young b04bdc960b Create tests for app/actions/fortify/UpdateUserProfileInformation 2025-07-01 23:08:45 -05:00
Matt Young 618ae79cd0 Create tests for app/actions/fortify/UpdateUserPassword 2025-07-01 22:00:13 -05:00
Matt Young 328aa110e2 Create tests for app/actions/fortify/UpdateUserPassword 2025-07-01 21:52:30 -05:00
Matt Young 373ad8b869 Create tests for app/actions/fortify/ResetUserPassword 2025-07-01 21:26:40 -05:00
Matt Young a5928350a7 Create tests for app/actions/fortify/CreateNewUser 2025-07-01 21:13:25 -05:00
Matt Young d994e906e1 SetHeadDirector testing 2025-07-01 20:25:43 -05:00
Matt Young d7134a948b Update SchoolController to use actions. 2025-07-01 18:52:33 -05:00
Matt Young 5ebef46be7 Create AssignUserToSchool action and test. 2025-07-01 18:40:44 -05:00
Matt Young 691d1d5f7c AddSchoolEmailDomain action and test. 2025-07-01 18:29:20 -05:00
Matt Young fa3df80e3c AddSchoolEmailDomain action and test. 2025-07-01 17:33:44 -05:00
Matt Young 9ae4b0388a Enforce unique school names
Rewrite admin school controler to use action when creating a school.
2025-07-01 11:43:36 -05:00
Matt Young bd207f8e4a Create school test and action created 2025-07-01 11:30:31 -05:00
Matt Young b4bf94d9f8 Fix audition factory to only create event if one is not provided. 2025-07-01 09:59:10 -05:00
Matt Young 42894c40c1 UpdateEntry action fully tested 2025-07-01 09:58:48 -05:00
Matt Young bed4e0671e UpdateEntry action fully tested 2025-07-01 09:44:31 -05:00
Matt Young 30862271f2 UpdateEntry tests 2025-07-01 02:30:06 -05:00
Matt Young 2f3c46973f Remove depricated action GetEntrySeatingResult 2025-07-01 00:21:31 -05:00
Matt Young 5ff3785f9f Finish DoublerDecision tests 2025-07-01 00:18:52 -05:00
Matt Young db22918ff8 Finis CreateEntries tests 2025-06-30 21:53:54 -05:00
Matt Young 0d8d876ba7 Rewrite DoublerDecision action.
Rewrite SeatAuditionFormController to use the action for making doubler decisions.
2025-06-30 18:26:12 -05:00
Matt Young 3a1b03a6c7 Define return types in RankAuditionEntries action. 2025-06-30 18:04:46 -05:00
Matt Young 115bd9b320 Fix error in SeatAuditionFormController that resulted in all unresolved doublers being declined when doing mass declines. 2025-06-30 17:58:04 -05:00
Matt Young f9fd6b1150 Merge branch 'testingRewrite' of https://github.com/okorpheus/auditionadmin into testingRewrite 2025-06-30 16:20:38 -05:00
Matt Young 69a2df6d6d Create app/actions/CreateEntriesTest 2025-06-30 16:18:50 -05:00
Matt Young b32ade6c7f Stash old tests 2025-06-30 16:18:40 -05:00
Matt Young e0c3a26411 Merge branch 'bugfix/Admin-EntryController-Fix-For-New-Scoring'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2025-06-30 15:59:26 -05:00
Matt Young 3319438c0d Fix error in admin EntryController related to updates in scoring. 2025-06-30 15:57:59 -05:00
Matt Young edb81496f3 Create app/actions/CreateEntriesTest 2025-06-30 15:35:47 -05:00
Matt Young 9bbcb2fae3 Stash old tests 2025-06-30 15:04:47 -05:00
Matt Young 4faeb63c95 Changes to allow testing. 2025-06-30 09:20:07 -05:00
Matt Young 307b879186 Block attempts to create a duplicate room name
Closes #103
2025-06-30 08:57:44 -05:00
Matt Young c4e8cbfe53 Remove depricated validateScores method from ScoringGuide model
Closes #34
2025-06-30 08:51:41 -05:00
Matt Young f0ad56e0d2 Block publication of results where no entries succeed.
Closes #94
2025-06-30 08:47:51 -05:00
Matt Young ad4c2e80b6 For unscored entries, show the number of judges that have entered a score for each entry.
Closes #92
2025-06-30 08:32:36 -05:00
Matt Young 9cf025154a Update TotalEntryScores action to recognize an insufficient number of judges. 2025-06-30 08:27:54 -05:00
Matt Young 790f725342 Allow lazy loading 2025-06-30 08:23:30 -05:00
Matt Young b2c4e5ec97 Added grade dropdown to edit student from.
Closes #105
2025-06-30 01:20:26 -05:00
Matt Young fc650b3be1 Clear caches when adding or removing a flag. Block adding or removing flags from published auditions
Closes #91
2025-06-30 01:13:02 -05:00
Matt Young d0aa29fb1a Merge branch 'scoringRewrite'
* scoringRewrite: (67 commits)
  Advancement working.
  Correctly show advancement screen.
  Correct isssue in RankAuditionEntries action for advancmenet.
  Switch settings to be stored in a static property instead of cache.
  Remove depricated code.
  Fix lazy loading issue when an audition is seated.
  Add TODO
  Allow for bluk declining seats
  add ability to fictionalize data
  rename sync-doublers console command
  Remove depricated code from bonusscore model
  When appropriate, include bonus score in ranking entrie. Show if an entry has bonus scores when appropriate.
  Add console command to force recalculation of scores
  Deal with bonus scores when calculating total scores.
  Migration to add bonus score related columns to the total scores table.
  Remove depricated code from EnterBonusScore action.
  Cleanup Debugbar Code
  Seating Publication Working
  Everything ready for seating the audition.
  add ability to mark no-shows and accept/decline doublers from the seating page.
  ...
2025-06-30 00:40:26 -05:00
Matt Young d0bd3f5092 Advancement working. 2025-06-30 00:38:42 -05:00
Matt Young 24e1c3d95e Correctly show advancement screen. 2025-06-29 23:57:10 -05:00
Matt Young c011d91615 Correct isssue in RankAuditionEntries action for advancmenet. 2025-06-29 23:51:50 -05:00
Matt Young 6f207edb0a Switch settings to be stored in a static property instead of cache. 2025-06-28 12:18:00 -05:00
Matt Young e14b678c74 Remove depricated code. 2025-06-28 09:04:04 -05:00
Matt Young 57780846e3 Fix lazy loading issue when an audition is seated. 2025-06-27 16:28:56 -05:00
Matt Young 62dab98906 Add TODO 2025-06-27 16:24:25 -05:00
Matt Young f2cb96dc0d Merge branch 'bonusScoreRewrite' into scoringRewrite 2025-06-26 18:34:06 -05:00
Matt Young 7670e91f43 Allow for bluk declining seats 2025-06-26 18:32:16 -05:00
Matt Young a3e8785767 add ability to fictionalize data 2025-06-26 11:00:13 -05:00
Matt Young 0bc80002bb rename sync-doublers console command 2025-06-26 10:35:01 -05:00
Matt Young abc86ba726 Remove depricated code from bonusscore model 2025-06-26 10:31:43 -05:00
Matt Young 04cfde353e When appropriate, include bonus score in ranking entrie. Show if an entry has bonus scores when appropriate. 2025-06-26 10:22:59 -05:00
Matt Young fd3855a775 Add console command to force recalculation of scores 2025-06-26 10:07:51 -05:00
Matt Young 86ec4f4062 Deal with bonus scores when calculating total scores. 2025-06-26 09:32:06 -05:00
Matt Young ee45499e7a Migration to add bonus score related columns to the total scores table. 2025-06-26 09:15:50 -05:00
Matt Young 6c52aa255c Remove depricated code from EnterBonusScore action. 2025-06-26 08:10:09 -05:00
Matt Young cee9f487bc Cleanup Debugbar Code 2025-06-26 04:08:35 -05:00
Matt Young 98378c6182 Seating Publication Working 2025-06-26 03:44:34 -05:00
Matt Young 14b6bb61c7 Everything ready for seating the audition. 2025-06-25 22:51:27 -05:00
Matt Young 0468cb5d11 add ability to mark no-shows and accept/decline doublers from the seating page. 2025-06-25 21:20:33 -05:00
Matt Young 5e687bcbc6 Modify EntryFlagController to use teh new enter no show action. 2025-06-25 15:38:57 -05:00
Matt Young fba625c316 Create action for entering no_show and failed_prelim flags 2025-06-25 15:25:10 -05:00
Matt Young e1719c64fa Seats can be declined from seating page. Doubler system functioning. 2025-06-24 09:24:53 -05:00
Matt Young 63b60e6bf5 Work on doubler blocks on seating page 2025-06-23 08:25:23 -05:00
Matt Young 88ef36d8be Cleanup on doubler model 2025-06-23 03:25:10 -05:00
Matt Young f0daa05fcf Model updates dealing with doublers. 2025-06-23 02:36:44 -05:00
Matt Young 630efaf00f remove entry totals from Doubler database. Save list of entries for each doubler. 2025-06-23 00:52:19 -05:00
Matt Young a27b8166e2 add entries column to doublers table migration 2025-06-23 00:42:53 -05:00
Matt Young 1f635e6ecf Set up an observer to update doublers whenever an entry is created, modified, or deleted. 2025-06-23 00:25:37 -05:00
Matt Young f3013670a3 Correct error in doubler class. Add artisan command to sync doublers. 2025-06-22 23:38:35 -05:00
Matt Young f3591e9a08 Correct errors in migration 2025-06-22 23:28:37 -05:00
Matt Young d9a7e97047 Doubler model and migration created 2025-06-22 23:19:38 -05:00
Matt Young cdd0d2bd50 Doubler model and migration stems 2025-06-20 12:59:25 -05:00
Matt Young b6d89f294d Initial work on doubler column 2025-06-20 12:52:46 -05:00
Matt Young 7754b6df12 Move SeatAuditionFormController away from being invokable 2025-06-20 11:12:29 -05:00
Matt Young ad24a67baa Progress on seating form 2025-06-17 00:43:50 -05:00
Matt Young 349da644b7 Rewrite RankAuditionEntries action and use it in the new seat audition form controller. 2025-06-15 15:44:05 -05:00
Matt Young b8ce2bc6db Rewrite scoring all auditions. 2025-06-15 14:33:55 -05:00
Matt Young 4f317f1458 Throttle recalculating scores. 2025-06-15 14:32:17 -05:00
Matt Young 33bca1cfdf Avoid division by zero errors. 2025-06-15 14:31:52 -05:00
Matt Young 49b609e9b7 Avoid a math inaccuracy. 2025-06-14 15:58:32 -05:00
Matt Young 6057211836 Don't count a failed prelim audition as an unscored entry. 2025-06-14 15:58:12 -05:00
Matt Young 40a9133a79 Don't allow lazy loading on development 2025-06-14 15:57:32 -05:00
Matt Young 34e22187dd Set foundation for new handling of doublers 2025-06-14 11:12:36 -05:00
Matt Young 250a3856ba Separate failed prelim and noshow flags 2025-06-14 08:40:20 -05:00
Matt Young 727d4d7048 Initial work on seating page rewrite 2025-06-12 23:28:31 -05:00
Matt Young fd198a9972 Calculate all total scores for an audition. 2025-06-12 19:03:03 -05:00
Matt Young 8647a66df8 Define Entry and EntryTotalScore relationships 2025-06-12 08:47:04 -05:00
Matt Young e79e7e222d Add action to total an entries scores. 2025-06-12 01:11:08 -05:00
Matt Young 3c545f0dce Make changes to total scores table to have subscores for seating and for advancement. 2025-06-12 00:09:05 -05:00
Matt Young f0f8038e8a Create entry total score model and table. 2025-06-11 23:32:16 -05:00
Matt Young e800937f4d remove erroroneous file 2025-06-11 23:22:46 -05:00
Matt Young 9331e61839 Remove depricated table and create total_scores table 2025-06-11 23:03:49 -05:00
Matt Young 58f29f326c Remove depricated files 2025-06-11 23:03:35 -05:00
Matt Young 783ec991b3 Rewrite EnterScore action to deal with both seating and advancement totals. 2025-06-11 21:59:44 -05:00
Matt Young 86f715f086 Add logging to EnterScore action. 2025-06-11 19:55:14 -05:00
Matt Young 036ed38d19 Rewrite admin score entry to use the new action. 2025-06-11 17:57:53 -05:00
Matt Young e47265badd EnterScore action working to add a total score when a score is saved by a judge. 2025-06-11 15:41:37 -05:00
Matt Young e6da41d1d9 Merge branch 'scoringRewrite' of https://github.com/okorpheus/auditionadmin into scoringRewrite 2025-06-11 15:24:28 -05:00
Matt Young 1d8a3ce739 Rewrite enter score action 2025-06-11 15:23:54 -05:00
Matt Young be84a084cc Add sheet_total column to score_sheets table 2025-06-11 15:23:54 -05:00
Matt Young e785d33a2d EntrySeeder update
# Conflicts:
#	database/seeders/EntrySeeder.php
2025-06-11 15:23:48 -05:00
Matt Young b6fd8fb71c Update index-room-card.blade.php
Update rooms view
Give delete room button some space
2025-06-11 15:15:13 -05:00
Matt Young 3cb837fa66 Update RoomController.php
Fix controller - set room id for auditions when creating room 0
2025-06-11 10:14:54 -05:00
Matt Young c2572414e6 Update EntrySeeder.php
Fix entry seeder
2025-06-11 10:13:04 -05:00
Matt Young 015f058601 Merge branch 'scoringRewrite' of https://github.com/okorpheus/auditionadmin into scoringRewrite 2025-06-11 07:59:59 -05:00
Matt Young df732c4f5a Rewrite enter score action 2025-06-11 07:58:24 -05:00
Matt Young d95e8e5772 Add sheet_total column to score_sheets table 2025-06-11 07:58:24 -05:00
Matt Young bcaab84ded EntrySeeder update 2025-06-11 07:58:24 -05:00
Matt Young 551f04588c Add 'Unassigned' room creation for unassigned auditions when the unassigned room does not exist. 2025-06-11 07:55:15 -05:00
Matt Young 4766141a2c Grammar cleanup 2025-06-11 07:38:02 -05:00
Matt Young 78e90cbe25 Rewrite enter score action 2025-06-11 07:27:48 -05:00
Matt Young 13ca712ce9 Add sheet_total column to score_sheets table 2025-06-10 23:46:49 -05:00
Matt Young 6b42f2c1fb EntrySeeder update 2025-06-10 23:35:21 -05:00
Matt Young 4fe4ea56d1 EntrySeeder format cleanup. 2025-06-10 23:06:56 -05:00
Matt 6c51d134de
Merge pull request #111 from okorpheus/EndYearProcedures
End year procedures implementation
2025-05-29 22:13:24 -05:00
454 changed files with 19578 additions and 3736 deletions

3
.gitignore vendored
View File

@ -20,3 +20,6 @@ yarn-error.log
/.vscode
/app/Http/Controllers/TestController.php
/resources/views/test.blade.php
/reports
/--cache-directory
/storage/debug.html

View File

@ -1,11 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests - paralell" type="PestRunConfigurationType">
<configuration default="false" name="tests - parallel" type="PestRunConfigurationType">
<option name="pestRunnerSettings">
<PestRunner directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases" />
<PestRunner directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases --coverage" />
</option>
<option name="runnerSettings">
<PhpTestRunnerSettings directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases" />
<PhpTestRunnerSettings directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases --coverage" />
</option>
<method v="2" />
</configuration>
</component>
</component>

View File

@ -0,0 +1,28 @@
<?php
namespace App\Actions\Development;
use App\Actions\Tabulation\EnterScore;
use App\Models\Entry;
class FakeScoresForEntry
{
public function __construct()
{
}
public function __invoke(Entry $entry): void
{
$scoreScribe = app(EnterScore::class);
$scoringGuide = $entry->audition->scoringGuide;
$subscores = $scoringGuide->subscores;
$judges = $entry->audition->judges;
foreach ($judges as $judge) {
$scoringArray = [];
foreach ($subscores as $subscore) {
$scoringArray[$subscore->id] = mt_rand(0, $subscore->max_score);
}
$scoreScribe($judge, $entry, $scoringArray);
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Actions\Draw;
use App\Models\Audition;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use function auditionLog;
class ClearDraw
{
/** @codeCoverageIgnore */
public function __invoke(Audition|collection $auditions): void
{
if ($auditions instanceof Audition) {
$this->clearDraw($auditions);
}
if ($auditions instanceof Collection) {
$this->clearDraws($auditions);
}
}
public function clearDraw(Audition $audition): void
{
$audition->removeFlag('drawn');
DB::table('entries')->where('audition_id', $audition->id)->update(['draw_number' => null]);
$message = 'Cleared draw for audition #'.$audition->id.' '.$audition->name;
$affected['auditions'] = [$audition->id];
auditionLog($message, $affected);
}
public function clearDraws(Collection $auditions): void
{
foreach ($auditions as $audition) {
$this->clearDraw($audition);
}
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Actions\Draw;
use App\Models\Audition;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class RunDraw
{
public function __invoke(Audition|Collection $auditions): void
{
if ($auditions instanceof Audition) {
// Single audition, run draw directly
$this->runDraw($auditions);
return;
} elseif ($auditions instanceof Collection) {
$this->runDrawMultiple($auditions);
return;
}
}
public function runDraw(Audition $audition): void
{
// start off by clearing any existing draw numbers in the audition
DB::table('entries')->where('audition_id', $audition->id)->update(['draw_number' => null]);
$randomizedEntries = $audition->entries->shuffle();
// Move entries flagged as no show to the end
[$noShowEntries, $otherEntries] = $randomizedEntries->partition(function ($entry) {
return $entry->hasFlag('no_show');
});
$randomizedEntries = $otherEntries->merge($noShowEntries);
// Save draw numbers back to the entries\
$nextNumber = 1;
foreach ($randomizedEntries as $index => $entry) {
$entry->update(['draw_number' => $nextNumber]);
$nextNumber++;
}
$audition->addFlag('drawn');
}
public function runDrawMultiple(Collection $auditions): void
{
// Eager load the 'entries' relationship on all auditions if not already loaded
$auditions->loadMissing('entries');
$auditions->each(fn ($audition) => $this->runDraw($audition));
}
}

View File

@ -2,14 +2,12 @@
namespace App\Actions\Entries;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ManageEntryException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\Student;
use function auth;
class CreateEntry
{
public function __construct()
@ -19,49 +17,49 @@ class CreateEntry
/**
* @throws ManageEntryException
*/
public function __invoke(Student|int $student, Audition|int $audition, string|array|null $entry_for = null)
{
return $this->createEntry($student, $audition, $entry_for);
public function __invoke(
Student|int $student,
Audition|int $audition,
$for_seating = false,
$for_advancement = false,
$late_fee_waived = false
) {
return $this->createEntry($student, $audition, $for_seating, $for_advancement, $late_fee_waived);
}
/**
* @throws ManageEntryException
*/
public function createEntry(Student|int $student, Audition|int $audition, string|array|null $entry_for = null)
{
public function createEntry(
Student|int $student,
Audition|int $audition,
$for_seating = false,
$for_advancement = false,
$late_fee_waived = false
): Entry {
if (is_int($student)) {
$student = Student::find($student);
}
if (is_int($audition)) {
$audition = Audition::find($audition);
}
if (! $entry_for) {
$entry_for = ['seating', 'advancement'];
}
$entry_for = collect($entry_for);
$this->verifySubmission($student, $audition);
if (! $for_advancement && ! $for_seating) {
$for_seating = true;
$for_advancement = true;
}
$entry = Entry::make([
'student_id' => $student->id,
'audition_id' => $audition->id,
'draw_number' => $this->checkDraw($audition),
'for_seating' => $entry_for->contains('seating'),
'for_advancement' => $entry_for->contains('advancement'),
'for_seating' => $for_seating,
'for_advancement' => $for_advancement,
]);
$entry->save();
if (auth()->user()) {
$message = 'Entered '.$entry->student->full_name().' from '.$entry->student->school->name.' in '.$entry->audition->name.'.';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'entries' => [$entry->id],
'students' => [$entry->student_id],
'auditions' => [$entry->audition_id],
'schools' => [$entry->student->school_id],
],
]);
if ($late_fee_waived) {
$entry->addFlag('late_fee_waived');
$entry->refresh();
}
return $entry;
@ -83,29 +81,29 @@ class CreateEntry
{
// Make sure it's a valid student
if (! $student || ! $student->exists()) {
throw new ManageEntryException('Invalid student provided');
throw new AuditionAdminException('Invalid student provided');
}
// Make sure the audition is valid
if (! $audition || ! $audition->exists()) {
throw new ManageEntryException('Invalid audition provided');
throw new AuditionAdminException('Invalid audition provided');
}
// A student can't enter the same audition twice
if (Entry::where('student_id', $student->id)->where('audition_id', $audition->id)->exists()) {
throw new ManageEntryException('That student is already entered in that audition');
throw new AuditionAdminException('That student is already entered in that audition');
}
// Can't enter a published audition
if ($audition->hasFlag('seats_published')) {
throw new ManageEntryException('Cannot add an entry to an audition where seats are published');
throw new AuditionAdminException('Cannot add an entry to an audition where seats are published');
}
if ($audition->hasFlag('advancement_published')) {
throw new ManageEntryException('Cannot add an entry to an audition where advancement is published');
throw new AuditionAdminException('Cannot add an entry to an audition where advancement is published');
}
// Verify the grade of the student is in range for the audition
if ($student->grade > $audition->maximum_grade) {
throw new ManageEntryException('The grade of the student exceeds the maximum for that audition');
throw new AuditionAdminException('The grade of the student exceeds the maximum for that audition');
}
if ($student->grade < $audition->minimum_grade) {
throw new ManageEntryException('The grade of the student does not meet the minimum for that audition');
throw new AuditionAdminException('The grade of the student does not meet the minimum for that audition');
}
}
}

View File

@ -3,19 +3,12 @@
namespace App\Actions\Entries;
use App\Exceptions\AuditionAdminException;
use App\Models\Doubler;
use App\Models\Entry;
use App\Services\DoublerService;
use Illuminate\Support\Facades\Cache;
class DoublerDecision
{
protected DoublerService $doublerService;
public function __construct(DoublerService $doublerService)
{
$this->doublerService = $doublerService;
}
/**
* @throws AuditionAdminException
*/
@ -34,25 +27,45 @@ class DoublerDecision
'decline' => $this->decline($entry),
default => throw new AuditionAdminException('Invalid decision specified')
};
if ($decision != 'accept' && $decision != 'decline') {
throw new AuditionAdminException('Invalid decision specified');
}
}
/**
* Accepts an entry for the given audition.
*
* This method ensures the entry is not already declined, and that the
* audition is not in a state where seats or advancement are published.
* If the entry is already declined, this method does nothing.
* If the audition is in a state where seats or advancement are published,
* this method throws an exception.
*
* This method also declines all other entries in the same audition,
* clearing the rank cache for the audition.
*
* @throws AuditionAdminException
*/
public function accept(Entry $entry): void
{
Cache::forget('audition'.$entry->audition_id.'seating');
Cache::forget('audition'.$entry->audition_id.'advancement');
if ($entry->hasFlag('declined')) {
throw new AuditionAdminException('Entry '.$entry->id.' is already declined');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot accept an entry in an audition where seats are published');
}
Cache::forget('rank_seating_'.$entry->audition_id);
// Decline all other entries and clear rank cache
$doublerInfo = $this->doublerService->simpleDoubleInfo($entry);
foreach ($doublerInfo as $doublerEntry) {
Cache::forget('audition'.$doublerEntry->audition_id.'seating');
/** @var Entry $doublerEntry */
if ($doublerEntry->id !== $entry->id) {
$doublerEntry->addFlag('declined');
// Process student entries
$doublerData = Doubler::findDoubler($entry->student_id, $entry->audition->event_id);
// Check each entry and see if it is unscored. We can't accept this entry if that is the case.
foreach ($doublerData->entries() as $doublerEntry) {
if (! $doublerEntry->totalScore && ! $doublerEntry->hasFlag('declined') && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim')) {
throw new AuditionAdminException('Cannot accept seating for '.$entry->student->full_name().' because student has unscored entries');
}
}
// Decline all other entries
foreach ($doublerData->entries() as $doublerEntry) {
Cache::forget('rank_seating_'.$doublerEntry->audition_id);
if ($doublerEntry->id !== $entry->id && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim') && ! $doublerEntry->hasFlag('declined')) {
$this->decline($doublerEntry);
}
}
}
@ -62,12 +75,21 @@ class DoublerDecision
*/
public function decline($entry): void
{
Cache::forget('audition'.$entry->audition_id.'seating');
Cache::forget('audition'.$entry->audition_id.'advancement');
// Entry cannot decline a seat twice
if ($entry->hasFlag('declined')) {
throw new AuditionAdminException('Entry is already declined');
throw new AuditionAdminException('Entry '.$entry->id.' is already declined');
}
Cache::forget('audition'.$entry->audition_id.'seating');
if (! $entry->totalScore) {
throw new AuditionAdminException('Cannot decline an unscored entry');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot decline an entry in an audition where seats are published');
}
// Flag this entry
$entry->addFlag('declined');
// Clear rank cache
Cache::forget('rank_seating_'.$entry->audition_id);
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Actions\Entries;
use App\Models\Entry;
use App\Models\Seat;
class GetEntrySeatingResult
{
public function __construct()
{
}
public function __invoke(Entry $entry): string
{
return $this->getResult($entry);
}
public function getResult(Entry $entry): string
{
if ($entry->hasFlag('failed_prelim')) {
return 'Failed Prelim';
}
if ($entry->hasFlag('no_show')) {
return 'No Show';
}
if ($entry->hasFlag('declined')) {
return 'Declined';
}
if ($entry->hasFlag('failed_prelim')) {
return 'Did not pass prelim';
}
$seat = Seat::where('entry_id', $entry->id)->first();
if ($seat) {
return $seat->ensemble->name.' '.$seat->seat;
}
return 'Entry not seated';
}
}

View File

@ -2,6 +2,7 @@
namespace App\Actions\Entries;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ManageEntryException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
@ -27,7 +28,7 @@ class UpdateEntry
/**
* @throws ManageEntryException
*/
public function __invoke(Entry $entry, array $updateData): void
public function __invoke(Entry|int $entry, array $updateData): void
{
$this->updateEntry($entry, $updateData);
}
@ -41,7 +42,7 @@ class UpdateEntry
$entry = Entry::find($entry);
}
if (! $entry || ! $entry->exists) {
throw new ManageEntryException('Invalid entry provided');
throw new AuditionAdminException('Invalid entry provided');
}
$this->entry = $entry;
if (array_key_exists('for_seating', $updateData)) {
@ -81,34 +82,34 @@ class UpdateEntry
$audition = Audition::find($audition);
}
if (! $audition || ! $audition->exists) {
throw new ManageEntryException('Invalid audition provided');
throw new AuditionAdminException('Invalid audition provided');
}
if ($this->entry->audition->hasFlag('seats_published')) {
throw new ManageEntryException('Cannot change the audition for an entry where seating for that entry\'s current audition is published');
throw new AuditionAdminException('Cannot change the audition for an entry where seating for that entry\'s current audition is published');
}
if ($this->entry->audition->hasFlag('advancement_published')) {
throw new ManageEntryException('Cannot change the audition for an entry where advancement for that entry\'s current audition is published');
throw new AuditionAdminException('Cannot change the audition for an entry where advancement for that entry\'s current audition is published');
}
if ($audition->hasFlag('seats_published')) {
throw new ManageEntryException('Cannot change the entry to an audition with published seating');
throw new AuditionAdminException('Cannot change the entry to an audition with published seating');
}
if ($audition->hasFlag('advancement_published')) {
throw new ManageEntryException('Cannot change the entry to an audition with published advancement');
throw new AuditionAdminException('Cannot change the entry to an audition with published advancement');
}
if ($this->entry->student->grade > $audition->maximum_grade) {
throw new ManageEntryException('The grade of the student exceeds the maximum for that audition');
throw new AuditionAdminException('The student is too old to enter that audition');
}
if ($this->entry->student->grade < $audition->minimum_grade) {
throw new ManageEntryException('The grade of the student does not meet the minimum for that audition');
throw new AuditionAdminException('The student is too young to enter that audition');
}
if ($this->entry->scoreSheets()->count() > 0) {
throw new ManageEntryException('Cannot change the audition for an entry with scores');
throw new AuditionAdminException('Cannot change the audition for an entry with scores');
}
if ($audition->id !== $this->entry->audition_id &&
Entry::where('student_id', $this->entry->student_id)
->where('audition_id', $audition->id)->exists()) {
throw new ManageEntryException('That student is already entered in that audition');
throw new AuditionAdminException('That student is already entered in that audition');
}
// Escape if we're not actually making a change
if ($this->entry->audition_id == $audition->id) {
@ -139,13 +140,13 @@ class UpdateEntry
}
if ($forSeating) {
if ($this->entry->audition->hasFlag('seats_published')) {
throw new ManageEntryException('Cannot add seating to an entry in an audition where seats are published');
throw new AuditionAdminException('Cannot add seating to an entry in an audition where seats are published');
}
$this->entry->for_seating = 1;
$this->log_message .= 'Entry '.$this->entry->id.' is entered for seating '.'<br>';
} else {
if ($this->entry->audition->hasFlag('seats_published')) {
throw new ManageEntryException('Cannot remove seating from an entry in an audition where seats are published');
throw new AuditionAdminException('Cannot remove seating from an entry in an audition where seats are published');
}
$this->entry->for_seating = 0;
$this->log_message .= 'Entry '.$this->entry->id.' is NOT entered for seating '.'<br>';
@ -162,13 +163,13 @@ class UpdateEntry
}
if ($forAdvancement) {
if ($this->entry->audition->hasFlag('advancement_published')) {
throw new ManageEntryException('Cannot add advancement to an entry in an audition where advancement is published');
throw new AuditionAdminException('Cannot add advancement to an entry in an audition where advancement is published');
}
$this->entry->for_advancement = 1;
$this->log_message .= 'Entry '.$this->entry->id.' is entered for '.auditionSetting('advanceTo').'<br>';
} else {
if ($this->entry->audition->hasFlag('advancement_published')) {
throw new ManageEntryException('Cannot remove advancement from an entry in an audition where advancement is published');
throw new AuditionAdminException('Cannot remove advancement from an entry in an audition where advancement is published');
}
$this->entry->for_advancement = 0;
$this->log_message .= 'Entry '.$this->entry->id.' is NOT entered for '.auditionSetting('advanceTo').'<br>';

View File

@ -2,7 +2,6 @@
namespace App\Actions\Fortify;
use App\Models\AuditLogEntry;
use App\Models\User;
use App\Rules\ValidRegistrationCode;
use Illuminate\Support\Facades\Hash;
@ -53,17 +52,6 @@ class CreateNewUser implements CreatesNewUsers
'password' => Hash::make($input['password']),
]);
$message = 'New User Registered - '.$input['email']
.'<br>Name: '.$input['first_name'].' '.$input['last_name']
.'<br>Judging Pref: '.$input['judging_preference']
.'<br>Cell Phone: '.$input['cell_phone'];
AuditLogEntry::create([
'user' => $input['email'],
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => $user->id],
]);
return $user;
}
}

View File

@ -2,7 +2,6 @@
namespace App\Actions\Fortify;
use App\Models\AuditLogEntry;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
@ -26,11 +25,6 @@ class ResetUserPassword implements ResetsUserPasswords
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
AuditLogEntry::create([
'user' => $user->email,
'ip_address' => request()->ip(),
'message' => 'Reset Password',
'affected' => ['users' => [$user->id]],
]);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Actions\Fortify;
use App\Exceptions\AuditionAdminException;
use App\Models\User;
class UpdateUserPrivileges
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(User|int $user, string $action, string $privilege): void
{
$this->setPrivilege($user, $action, $privilege);
}
/**
* @throws AuditionAdminException
*/
public function setPrivilege(User|int $user, string $action, string $privilege): void
{
if (is_int($user)) {
$user = User::findOrFail($user);
}
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (! in_array($action, ['grant', 'revoke'])) {
throw new AuditionAdminException('Invalid Action');
}
$field = match ($privilege) {
'admin' => 'is_admin',
'tab' => 'is_tab',
default => throw new AuditionAdminException('Invalid Privilege'),
};
if ($user->$field == 1 && $action == 'revoke') {
$user->$field = 0;
$user->save();
}
if ($user->$field == 0 && $action == 'grant') {
$user->$field = 1;
$user->save();
}
}
}

View File

@ -2,7 +2,6 @@
namespace App\Actions\Fortify;
use App\Models\AuditLogEntry;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
@ -45,16 +44,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
'email' => $input['email'],
])->save();
}
$message = 'Updated user #'.$user->id.' - '.$user->email
.'<br>Name: '.$user->full_name()
.'<br>Judging Pref: '.$user->judging_preference
.'<br>Cell Phone: '.$user->cell_phone;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
}
/**
@ -74,17 +64,6 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
'email_verified_at' => null,
])->save();
$user->refresh();
$message = 'Updated user #'.$user->id.' - '.$oldEmail
.'<br>Name: '.$user->full_name()
.'<br>Email: '.$user->email
.'<br>Judging Pref: '.$user->judging_preference
.'<br>Cell Phone: '.$user->cell_phone;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
$user->sendEmailVerificationNotification();
}

View File

@ -6,6 +6,10 @@ use App\Models\Entry;
use App\Models\Room;
use Illuminate\Support\Collection;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for PrintSignInSheets
class PrintSignInSheets
{
protected $pdf;
@ -74,11 +78,15 @@ class PrintSignInSheets
public function addEntryRow(Entry $entry)
{
$nameLine = $entry->student->full_name();
if ($entry->student->isDoublerInEvent($entry->audition->event_id)) {
$nameLine .= ' (D)';
}
$this->pdf->Cell($this->columnWidth['id'], $this->bodyRowHeight, $entry->id, 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['instrument'], $this->bodyRowHeight, $entry->audition->name, 1, 0,
'L');
$this->pdf->Cell($this->columnWidth['drawNumber'], $this->bodyRowHeight, $entry->draw_number, 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['name'], $this->bodyRowHeight, $entry->student->full_name(), 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['name'], $this->bodyRowHeight, $nameLine, 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['school'], $this->bodyRowHeight, $entry->student->school->name, 1, 0, 'L');
$this->pdf->Cell(0, $this->bodyRowHeight, ' ', 1, 1, 'L');
}

View File

@ -5,6 +5,10 @@ namespace App\Actions\Print;
use App\Models\Ensemble;
use Codedge\Fpdf\Fpdf\Fpdf;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for PrintStandNameTags
class PrintStandNameTags
{
public function __construct()

View File

@ -8,6 +8,10 @@ use Illuminate\Support\Collection;
use function auditionSetting;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for QuarterPageCards
class QuarterPageCards implements PrintCards
{
protected $pdf;
@ -54,17 +58,37 @@ class QuarterPageCards implements PrintCards
$this->pdf->Cell(4.5, .5, $entry->audition->name.' #'.$entry->draw_number);
// Fill in student information
$nameLine = $entry->student->full_name();
if ($entry->student->isDoublerInEvent($entry->audition->event_id)) {
$nameLine .= ' (D)';
}
$this->pdf->SetFont('Arial', '', 10);
$xLoc = $this->offset[$this->quadOn][0] + 1;
$yLoc = $this->offset[$this->quadOn][1] + 3.1;
$this->pdf->setXY($xLoc, $yLoc);
$this->pdf->Cell(4.5, .25, $entry->student->full_name());
$this->pdf->Cell(4.5, .25, $nameLine);
$this->pdf->setXY($xLoc, $yLoc + .25);
$this->pdf->Cell(4.5, .25, $entry->student->school->name);
$this->pdf->setXY($xLoc, $yLoc + .5);
if (! is_null($entry->audition->room_id)) {
$this->pdf->Cell(4.5, .25, $entry->audition->room->name);
}
if (auditionSetting('advanceTo')) {
$as = false;
$this->pdf->setXY($xLoc, $yLoc - 1);
$auditioningFor = 'Auditioning for: ';
if ($entry->for_seating) {
$auditioningFor .= auditionSetting('auditionAbbreviation');
$as = true;
}
if ($entry->for_advancement) {
if ($as) {
$auditioningFor .= ' / ';
}
$auditioningFor .= auditionSetting('advanceTo');
}
$this->pdf->Cell(4.5, .25, $auditioningFor);
}
$this->quadOn++;
}

View File

@ -4,6 +4,10 @@ namespace App\Actions\Print;
use Codedge\Fpdf\Fpdf\Fpdf;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for signInPDF
class signInPDF extends Fpdf
{
public $roomOn;

View File

@ -6,6 +6,10 @@ use App\Actions\Tabulation\RankAuditionEntries;
use App\Models\Room;
use Illuminate\Support\Facades\App;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for ExportEntryData
class ExportEntryData
{
public function __construct()

View File

@ -7,6 +7,10 @@ use App\Models\Event;
use App\Models\Seat;
use Illuminate\Support\Facades\App;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for GetExportData
class GetExportData
{
public function __construct()
@ -29,7 +33,7 @@ class GetExportData
foreach ($events as $event) {
$auditions = $event->auditions;
foreach ($auditions as $audition) {
$entries = $ranker->rank('seating', $audition);
$entries = $ranker($audition, 'seating');
foreach ($entries as $entry) {
$thisRow = $audition->name.',';
$thisRow .= $entry->raw_rank ?? '';
@ -37,7 +41,7 @@ class GetExportData
$thisRow .= $entry->student->full_name().',';
$thisRow .= $entry->student->school->name.',';
$thisRow .= $entry->student->grade.',';
$thisRow .= $entry->score_totals[0] ?? '';
$thisRow .= $entry->totalScore->seating_total ?? '';
$thisRow .= ',';
if ($entry->hasFlag('failed_prelim')) {
$thisRow .= 'Failed Prelim';

View File

@ -0,0 +1,35 @@
<?php
namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\SchoolEmailDomain;
class AddSchoolEmailDomain
{
public function __construct()
{
}
public function __invoke(School $school, string $domain): void
{
$this->addDomain($school, $domain);
}
public function addDomain(School $school, string $domain): void
{
if (! School::where('id', $school->id)->exists()) {
throw new AuditionAdminException('School does not exist');
}
if (SchoolEmailDomain::where('domain', $domain)->where('school_id', $school->id)->exists()) {
return;
}
SchoolEmailDomain::create([
'domain' => $domain,
'school_id' => $school->id,
]);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\User;
class AssignUserToSchool
{
public function __invoke(User $user, School|int|null $school): void
{
$this->assign($user, $school);
}
public function assign(User $user, School|int|null $school, bool $addDomainToSchool = true): void
{
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (is_int($school)) {
$school = School::find($school);
}
if (is_null($school)) {
$user->update([
'school_id' => null,
]);
return;
}
if (is_null($school) || ! School::where('id', $school->id)->exists()) {
throw new AuditionAdminException('School does not exist');
}
$domainRecorder = app(AddSchoolEmailDomain::class);
if ($addDomainToSchool) {
$domainRecorder($school, $user->emailDomain());
}
$user->update([
'school_id' => $school->id,
]);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
class CreateSchool
{
public function __invoke(
string $name,
?string $address = null,
?string $city = null,
?string $state = null,
?string $zip = null
): School {
return $this->create($name, $address, $city, $state, $zip);
}
public function create(
string $name,
?string $address = null,
?string $city = null,
?string $state = null,
?string $zip = null
): School {
if (School::where('name', $name)->exists()) {
throw new AuditionAdminException('The school '.$name.' already exists');
}
$newSchool = School::create([
'name' => $name,
'address' => $address,
'city' => $city,
'state' => $state,
'zip' => $zip,
]);
if (auth()->user()) {
$message = 'Created school '.$newSchool->name;
$affects = ['schools' => [$newSchool->id]];
auditionLog($message, $affects);
}
return $newSchool;
}
}

View File

@ -3,7 +3,6 @@
namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\User;
use function auditionLog;
@ -15,9 +14,9 @@ class SetHeadDirector
{
}
public function __invoke(User $user, School $school): void
public function __invoke(User $user): void
{
$this->setHeadDirector($user, $school);
$this->setHeadDirector($user);
}
/**
@ -25,6 +24,14 @@ class SetHeadDirector
*/
public function setHeadDirector(User $user): void
{
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if ($user->hasFlag('head_director')) {
return;
}
if (is_null($user->school_id)) {
throw new AuditionAdminException('User is not associated with a school');
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Actions\Students;
use App\Exceptions\AuditionAdminException;
use App\Models\Student;
use Arr;
class CreateStudent
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(array $newData): Student
{
// $newData[] must include keys first_name, last_name, grade - throw an exception if it does not
foreach (['first_name', 'last_name', 'grade'] as $key) {
if (! Arr::has($newData, $key)) {
throw new AuditionAdminException('Missing required data');
}
}
if (! Arr::has($newData, 'school_id')) {
$newData['school_id'] = auth()->user()->school_id;
}
if (Student::where('first_name', $newData['first_name'])->where('last_name', $newData['last_name'])
->where('school_id', $newData['school_id'])->exists()) {
throw new AuditionAdminException('Student already exists');
}
return Student::create([
'first_name' => $newData['first_name'],
'last_name' => $newData['last_name'],
'grade' => $newData['grade'],
'school_id' => $newData['school_id'],
'optional_data' => $newData['optional_data'] ?? null,
]);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Actions\Students;
use App\Exceptions\AuditionAdminException;
use App\Models\Student;
use Arr;
class UpdateStudent
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(Student $student, array $newData): bool
{
// $newData[] must include keys first_name, last_name, grade - throw an exception if it does not
foreach (['first_name', 'last_name', 'grade'] as $key) {
if (! Arr::has($newData, $key)) {
throw new AuditionAdminException('Missing required data');
}
}
if (! Arr::has($newData, 'school_id')) {
$newData['school_id'] = auth()->user()->school_id;
}
if (Student::where('first_name', $newData['first_name'])
->where('last_name', $newData['last_name'])
->where('school_id', $newData['school_id'])
->where('id', '!=', $student->id)
->exists()) {
throw new AuditionAdminException('Student already exists');
}
return $student->update([
'first_name' => $newData['first_name'],
'last_name' => $newData['last_name'],
'grade' => $newData['grade'],
'school_id' => $newData['school_id'],
'optional_data' => $newData['optional_data'] ?? null,
]);
}
}

View File

@ -1,99 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Services\AuditionService;
use App\Services\EntryService;
use Illuminate\Support\Facades\Cache;
class AllJudgesCount implements CalculateEntryScore
{
protected CalculateScoreSheetTotal $calculator;
protected AuditionService $auditionService;
protected EntryService $entryService;
public function __construct(CalculateScoreSheetTotal $calculator, AuditionService $auditionService, EntryService $entryService)
{
$this->calculator = $calculator;
$this->auditionService = $auditionService;
$this->entryService = $entryService;
}
public function calculate(string $mode, Entry $entry): array
{
$cacheKey = 'entryScore-'.$entry->id.'-'.$mode;
return Cache::remember($cacheKey, 300, function () use ($mode, $entry) {
$this->isEntryANoShow($entry);
$this->basicValidation($mode, $entry);
$this->areAllJudgesIn($entry);
$this->areAllJudgesValid($entry);
return $this->getJudgeTotals($mode, $entry);
});
}
protected function getJudgeTotals($mode, Entry $entry)
{
$scores = [];
foreach ($this->auditionService->getJudges($entry->audition) as $judge) {
$scores[] = $this->calculator->__invoke($mode, $entry, $judge);
}
$sums = [];
// Sum each subscore from the judges
foreach ($scores as $score) {
$index = 0;
foreach ($score as $value) {
$sums[$index] = $sums[$index] ?? 0;
$sums[$index] += $value;
$index++;
}
}
return $sums;
}
protected function basicValidation($mode, $entry): void
{
if ($mode !== 'seating' && $mode !== 'advancement') {
throw new TabulationException('Mode must be seating or advancement');
}
if (! $this->entryService->entryExists($entry)) {
throw new TabulationException('Invalid entry specified');
}
}
protected function areAllJudgesIn(Entry $entry): void
{
$assignedJudgeCount = $this->auditionService->getJudges($entry->audition)->count();
if ($entry->scoreSheets->count() !== $assignedJudgeCount) {
throw new TabulationException('Not all score sheets are in');
}
}
protected function areAllJudgesValid(Entry $entry): void
{
$validJudgeIds = $this->auditionService->getJudges($entry->audition)->pluck('id')->sort()->toArray();
$existingJudgeIds = $entry->scoreSheets->pluck('user_id')->sort()->toArray();
if ($validJudgeIds !== $existingJudgeIds) {
throw new TabulationException('Score exists from a judge not assigned to this audition');
}
}
protected function isEntryANoShow(Entry $entry): void
{
if ($entry->hasFlag('no_show')) {
throw new TabulationException('No Show');
}
}
}

View File

@ -1,161 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\BonusScore;
use App\Models\CalculatedScore;
use App\Models\Entry;
use App\Services\AuditionService;
use App\Services\EntryService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use function auditionSetting;
class AllowForOlympicScoring implements CalculateEntryScore
{
protected CalculateScoreSheetTotal $calculator;
protected AuditionService $auditionService;
protected EntryService $entryService;
public function __construct(
CalculateScoreSheetTotal $calculator,
AuditionService $auditionService,
EntryService $entryService
) {
$this->calculator = $calculator;
$this->auditionService = $auditionService;
$this->entryService = $entryService;
}
public function calculate(string $mode, Entry $entry): array
{
$calculated = CalculatedScore::where('entry_id', $entry->id)->where('mode', $mode)->first();
if ($calculated) {
return $calculated->calculatedScore;
}
$cacheKey = 'entryScore-'.$entry->id.'-'.$mode;
return Cache::remember($cacheKey, 300, function () use ($mode, $entry) {
$this->basicValidation($mode, $entry);
$this->isEntryANoShow($entry);
$this->areAllJudgesIn($entry);
$this->areAllJudgesValid($entry);
$calculatedScores = $this->getJudgeTotals($mode, $entry);
CalculatedScore::create([
'entry_id' => $entry->id,
'mode' => $mode,
'calculatedScore' => $calculatedScores,
]);
return $calculatedScores;
// return $this->getJudgeTotals($mode, $entry);
});
}
protected function getJudgeTotals($mode, Entry $entry): array
{
$scores = [];
foreach ($this->auditionService->getJudges($entry->audition) as $judge) {
$scores[] = $this->calculator->__invoke($mode, $entry, $judge);
}
// sort the scores array by the total score
usort($scores, function ($a, $b) {
return $a[0] <=> $b[0];
});
// we can only really do olympic scoring if there are at least 3 scores
if (count($scores) >= 3 && auditionSetting('olympic_scoring')) {
// remove the highest and lowest scores
array_pop($scores);
array_shift($scores);
}
$sums = [];
// Sum each subscore from the judges
foreach ($scores as $score) {
$index = 0;
foreach ($score as $value) {
$sums[$index] = $sums[$index] ?? 0;
$sums[$index] += $value;
$index++;
}
}
// add the bonus points for a seating mode
if ($mode === 'seating' && $sums) {
$sums[0] += $this->getBonusPoints($entry);
}
return $sums;
}
protected function getBonusPoints(Entry $entry)
{
$bonusScoreDefinition = $entry->audition->bonusScore()->first();
if (! $bonusScoreDefinition) {
return 0;
}
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
$bonusJudges = $bonusScoreDefinition->judges;
$bonusScoreSheets = BonusScore::where('entry_id', $entry->id)->get();
foreach ($bonusScoreSheets as $sheet) {
if (! $bonusJudges->contains($sheet->user_id)) {
throw new TabulationException('Entry has a bonus score from unassigned judge');
}
}
// sum the score property of the $bonusScoreSheets
return $bonusScoreSheets->sum('score');
}
protected function basicValidation($mode, $entry): void
{
if ($mode !== 'seating' && $mode !== 'advancement') {
throw new TabulationException('Mode must be seating or advancement');
}
if (! $this->entryService->entryExists($entry)) {
throw new TabulationException('Invalid entry specified');
}
}
protected function areAllJudgesIn(Entry $entry): void
{
$assignedJudgeCount = $this->auditionService->getJudges($entry->audition)->count();
if ($entry->scoreSheets->count() !== $assignedJudgeCount) {
throw new TabulationException('Not all score sheets are in');
}
}
protected function areAllJudgesValid(Entry $entry): void
{
$validJudgeIds = $this->auditionService->getJudges($entry->audition)->pluck('id')->toArray();
$existingJudgeIds = $entry->scoreSheets->pluck('user_id')->toArray();
if (array_diff($existingJudgeIds, $validJudgeIds)) {
Log::debug('EntryID: '.$entry->id);
Log::debug('Valid judge ids: ('.gettype($validJudgeIds).') '.json_encode($validJudgeIds));
Log::debug('Existing judge ids: ('.gettype($existingJudgeIds).') '.json_encode($existingJudgeIds));
throw new TabulationException('Score exists from a judge not assigned to this audition');
}
}
protected function isEntryANoShow(Entry $entry): void
{
if ($entry->hasFlag('failed_prelim')) {
throw new TabulationException('Failed Prelim');
}
if ($entry->hasFlag('no_show')) {
throw new TabulationException('No Show');
}
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Audition;
use App\Models\Entry;
class CalculateAuditionScores
{
public function __construct()
{
}
public function __invoke(Audition $audition): void
{
$totaler = app(TotalEntryScores::class);
$scores_required = $audition->judges->count();
$pending_entries = Entry::where('audition_id', $audition->id)
->has('scoreSheets', '=', $scores_required)
->whereDoesntHave('totalScore')
->with('audition.scoringGuide.subscores')
->get();
foreach ($pending_entries as $entry) {
$totaler->__invoke($entry);
}
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Entry;
interface CalculateEntryScore
{
public function calculate(string $mode, Entry $entry): array;
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Entry;
use App\Models\User;
interface CalculateScoreSheetTotal
{
public function __invoke(string $mode, Entry $entry, User $judge): array;
}

View File

@ -1,67 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Models\ScoreSheet;
use App\Models\User;
use App\Services\AuditionService;
use App\Services\EntryService;
use App\Services\UserService;
class CalculateScoreSheetTotalDivideByTotalWeights implements CalculateScoreSheetTotal
{
protected AuditionService $auditionService;
protected EntryService $entryService;
protected UserService $userService;
public function __construct(AuditionService $auditionService, EntryService $entryService, UserService $userService)
{
$this->auditionService = $auditionService;
$this->entryService = $entryService;
$this->userService = $userService;
}
public function __invoke(string $mode, Entry $entry, User $judge): array
{
$this->basicValidations($mode, $entry, $judge);
$scoreSheet = ScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
if (! $scoreSheet) {
throw new TabulationException('No score sheet by that judge for that entry');
}
$subscores = $this->auditionService->getSubscores($entry->audition, $mode);
$scoreTotal = 0;
$weightsTotal = 0;
$scoreArray = [];
foreach ($subscores as $subscore) {
$weight = $subscore['weight'];
$score = $scoreSheet->subscores[$subscore->id]['score'];
$scoreArray[] = $score;
$scoreTotal += ($score * $weight);
$weightsTotal += $weight;
}
$finalScore = $scoreTotal / $weightsTotal;
// put $final score at the beginning of the $ScoreArray
array_unshift($scoreArray, $finalScore);
return $scoreArray;
}
protected function basicValidations($mode, $entry, $judge): void
{
if ($mode !== 'seating' and $mode !== 'advancement') {
throw new TabulationException('Invalid mode requested. Mode must be seating or advancement');
}
if (! $this->entryService->entryExists($entry)) {
throw new TabulationException('Invalid entry provided');
}
if (! $this->userService->userExists($judge)) {
throw new TabulationException('Invalid judge provided');
}
}
}

View File

@ -1,74 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Models\ScoreSheet;
use App\Models\User;
use App\Services\AuditionService;
use App\Services\EntryService;
use App\Services\UserService;
class CalculateScoreSheetTotalDivideByWeightedPossible implements CalculateScoreSheetTotal
{
protected AuditionService $auditionService;
protected EntryService $entryService;
protected UserService $userService;
public function __construct(AuditionService $auditionService, EntryService $entryService, UserService $userService)
{
$this->auditionService = $auditionService;
$this->entryService = $entryService;
$this->userService = $userService;
}
public function __invoke(string $mode, Entry $entry, User $judge): array
{
$this->basicValidations($mode, $entry, $judge);
$scoreSheet = ScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
if (! $scoreSheet) {
throw new TabulationException('No score sheet by that judge for that entry');
}
$subscores = $this->auditionService->getSubscores($entry->audition, $mode);
$scoreTotal = 0;
$weightsTotal = 0;
$weightedMaxPossible = 0;
$scoreArray = [];
foreach ($subscores as $subscore) {
$weight = $subscore['weight'];
$score = $scoreSheet->subscores[$subscore->id]['score'];
$maxPossible = $subscore['max_score'];
$scoreArray[] = $score;
$scoreTotal += ($score * $weight);
$weightsTotal += $weight;
$weightedMaxPossible += $maxPossible;
}
if ($weightedMaxPossible > 0) {
$finalScore = ($scoreTotal / $weightedMaxPossible) * 100;
} else {
$finalScore = 0;
}
// put $final score at the beginning of the $ScoreArray
array_unshift($scoreArray, $finalScore);
return $scoreArray;
}
protected function basicValidations($mode, $entry, $judge): void
{
if ($mode !== 'seating' and $mode !== 'advancement') {
throw new TabulationException('Invalid mode requested. Mode must be seating or advancement');
}
if (! $this->entryService->entryExists($entry)) {
throw new TabulationException('Invalid entry provided');
}
if (! $this->userService->userExists($judge)) {
throw new TabulationException('Invalid judge provided');
}
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Entry;
class CheckPrelimResult
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(Entry $entry, bool $recalc = false): string
{
if ($recalc) {
$entry->removeFlag('passed_prelim');
$entry->removeFlag('failed_prelim');
}
if (! $entry->exists) {
throw new AuditionAdminException('Entry does not exist');
}
if (! $entry->audition->prelimDefinition) {
throw new AuditionAdminException('Entry does not have a prelim');
}
if ($entry->hasFlag('failed_prelim') || $entry->hasFlag('passed_prelim')) {
return 'noChange';
}
if (! $entry->audition->prelimDefinition->room || $entry->audition->prelimDefinition->room->judges()->count() == 0) {
return 'noJudgesAssigned';
}
$scoresRequired = $entry->audition->prelimDefinition->room->judges()->count();
$scoresAssigned = $entry->prelimScoreSheets()->count();
if ($scoresAssigned < $scoresRequired) {
return 'missing'.$scoresRequired - $scoresAssigned.'scores';
}
$totalScore = 0;
foreach ($entry->prelimScoreSheets as $scoreSheet) {
$totalScore += $scoreSheet->total;
}
$averageScore = $totalScore / $scoresAssigned;
if ($averageScore >= $entry->audition->prelimDefinition->passing_score) {
$entry->addFlag('passed_prelim');
return 'markedPassed';
} else {
$entry->addFlag('failed_prelim');
return 'markedFailed';
}
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Doubler;
use App\Models\Event;
use App\Models\Student;
use function collect;
class DoublerSync
{
public function __construct()
{
}
/**
* Sync the Doubler records for the given event. If no event is provided, sync Doubler records for all events.
*/
public function __invoke(Event|int|null $event = null): void
{
if ($event) {
$this->syncForEvent($event);
} else {
$this->syncAllDoublers();
}
}
public function syncForEvent(Event|int $eventId): void
{
if ($eventId instanceof Event) {
$eventId = $eventId->id;
}
// Get students with multiple entries in this event's auditions
$studentsWithMultipleEntries = Student::query()
->select('students.id')
->join('entries', 'students.id', '=', 'entries.student_id')
->join('auditions', 'entries.audition_id', '=', 'auditions.id')
->where('auditions.event_id', $eventId)
->groupBy('students.id')
->havingRaw('COUNT(entries.id) > 1')
->with('entries')
->get();
Doubler::where('event_id', $eventId)->delete();
foreach ($studentsWithMultipleEntries as $student) {
// Get entries that are not declined. If only one, they're our accepted entry.
$entryList = collect(); // List of entry ids for th is student in this event
$undecidedEntries = collect(); // List of entry ids that are not declined, no-show, or failed prelim
$entryList = $student->entriesForEvent($eventId)->pluck('id');
$undecidedEntries = $student->entriesForEvent($eventId)->filter(function ($entry) {
return ! $entry->hasFlag('declined')
&& ! $entry->hasFlag('no_show')
&& ! $entry->hasFlag('failed_prelim');
})->pluck('id');
if ($undecidedEntries->count() < 2) {
$acceptedEntryId = $undecidedEntries->first();
} else {
$acceptedEntryId = null;
}
// Create or update the doubler record
Doubler::create([
'student_id' => $student->id,
'event_id' => $eventId,
'entries' => $entryList,
'accepted_entry' => $acceptedEntryId,
]);
}
// remove doubler records for students who no longer have multiple entries
Doubler::where('event_id', $eventId)
->whereNotIn('student_id', $studentsWithMultipleEntries->pluck('id'))
->delete();
}
public function syncAllDoublers(): void
{
$events = Event::all();
foreach ($events as $event) {
$this->syncForEvent($event);
}
}
}

View File

@ -4,32 +4,27 @@
namespace App\Actions\Tabulation;
use App\Exceptions\ScoreEntryException;
use App\Exceptions\AuditionAdminException;
use App\Models\BonusScore;
use App\Models\CalculatedScore;
use App\Models\Entry;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\App;
class EnterBonusScore
{
public function __construct()
{
}
public function __invoke(User $judge, Entry $entry, int $score): void
{
$getRelatedEntries = App::make(GetBonusScoreRelatedEntries::class);
$this->basicValidations($judge, $entry);
// Verify there is a need for a bonus score
if ($entry->audition->bonusScore->count() === 0) {
throw new AuditionAdminException('The entries audition does not accept bonus scores');
}
$this->validateJudgeValidity($judge, $entry, $score);
$entries = $getRelatedEntries($entry);
// Create the score for each related entry
foreach ($entries as $relatedEntry) {
// Also delete any cached scores
CalculatedScore::where('entry_id', $relatedEntry->id)->delete();
BonusScore::create([
'entry_id' => $relatedEntry->id,
'user_id' => $judge->id,
@ -40,43 +35,18 @@ class EnterBonusScore
}
protected function getRelatedEntries(Entry $entry): Collection
{
$bonusScore = $entry->audition->bonusScore->first();
$relatedAuditions = $bonusScore->auditions;
// Get all entries that have a student_id equal to that of entry and an audition_id in the related auditions
return Entry::where('student_id', $entry->student_id)
->whereIn('audition_id', $relatedAuditions->pluck('id'))
->get();
}
protected function basicValidations(User $judge, Entry $entry): void
{
if (! $judge->exists) {
throw new ScoreEntryException('Invalid judge provided');
}
if (! $entry->exists) {
throw new ScoreEntryException('Invalid entry provided');
}
if ($entry->audition->bonusScore->count() === 0) {
throw new ScoreEntryException('Entry does not have a bonus score');
}
}
protected function validateJudgeValidity(User $judge, Entry $entry, $score): void
{
if (BonusScore::where('entry_id', $entry->id)->where('user_id', $judge->id)->exists()) {
throw new ScoreEntryException('That judge has already scored that entry');
throw new AuditionAdminException('That judge has already scored that entry');
}
$bonusScore = $entry->audition->bonusScore->first();
if (! $bonusScore->judges->contains($judge)) {
throw new ScoreEntryException('That judge is not assigned to judge that bonus score');
throw new AuditionAdminException('That judge is not assigned to judge that bonus score');
}
if ($score > $bonusScore->max_score) {
throw new ScoreEntryException('That score exceeds the maximum');
throw new AuditionAdminException('That score exceeds the maximum');
}
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\BonusScore;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\ScoreSheet;
use Illuminate\Support\Facades\DB;
use function auditionLog;
class EnterNoShow
{
/**
* Handles the no-show or failed-prelim flagging for a given entry.
*
* This method ensures the specified flag type is valid and validates
* that the action can be performed based on the associated audition's state.
* Deletes related score records and applies the specified flag ('no_show'
* or 'failed_prelim') to the entry, returning a success message.
*
* @param Entry $entry The entry being flagged.
* @param string $flagType The type of flag to apply ('no-show' or 'failed-prelim').
* @return string A confirmation message about the flagging operation.
*
* @throws AuditionAdminException If an invalid flag type is provided,
* or the action violates business rules.
*/
public function __invoke(Entry $entry, string $flagType = 'noshow'): string
{
if ($flagType !== 'noshow' && $flagType !== 'failprelim') {
throw new AuditionAdminException('Invalid flag type');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot enter a no-show for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot enter a no-show for an entry in an audition where advancement is published');
}
DB::table('score_sheets')->where('entry_id', $entry->id)->delete();
ScoreSheet::where('entry_id', $entry->id)->delete();
BonusScore::where('entry_id', $entry->id)->delete();
EntryTotalScore::where('entry_id', $entry->id)->delete();
if ($flagType == 'failprelim') {
$msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').';
$entry->addFlag('failed_prelim');
} else {
$entry->addFlag('no_show');
$msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').';
}
$affected = [
['entries', [$entry->id]],
['auditions', [$entry->audition_id]],
['students', [$entry->student_id]],
];
auditionLog($msg, $affected);
return $msg;
}
}

View File

@ -0,0 +1,137 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use App\Models\User;
use DB;
use function auditionLog;
class EnterPrelimScore
{
public function __invoke(
User $user,
Entry $entry,
array $scores,
PrelimScoreSheet|false $prelimScoreSheet = false
): PrelimScoreSheet {
$scores = collect($scores);
// Basic Validity Checks
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (! Entry::where('id', $entry->id)->exists()) {
throw new AuditionAdminException('Entry does not exist');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot score an entry in an audition where seats are published');
}
// Check if the entries audition has a prelim definition
if (! PrelimDefinition::where('audition_id', $entry->audition->id)->exists()) {
throw new AuditionAdminException('The entries audition does not have a prelim');
}
$prelimDefinition = PrelimDefinition::where('audition_id', $entry->audition->id)->first();
// Don't allow changes to prelims scores if the entry has a finals score
if ($entry->scoreSheets()->count() > 0) {
throw new AuditionAdminException('Cannot change prelims scores for an entry that has finals scores');
}
// Check that the specified user is assigned to judge this entry in prelims
$check = DB::table('room_user')
->where('user_id', $user->id)
->where('room_id', $prelimDefinition->room_id)->exists();
if (! $check) {
throw new AuditionAdminException('This judge is not assigned to judge this entry in prelims');
}
// Check if a score already exists
if (! $prelimScoreSheet) {
if (PrelimScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
throw new AuditionAdminException('That judge has already entered a prelim score for that entry');
}
} else {
if ($prelimScoreSheet->user_id != $user->id) {
throw new AuditionAdminException('Existing score sheet is from a different judge');
}
if ($prelimScoreSheet->entry_id != $entry->id) {
throw new AuditionAdminException('Existing score sheet is for a different entry');
}
}
// Check the validity of submitted subscores, format array for storage, and sum score
$subscoresRequired = $prelimDefinition->scoringGuide->subscores;
$subscoresStorageArray = [];
$totalScore = 0;
$maxPossibleTotal = 0;
if ($scores->count() !== $subscoresRequired->count()) {
throw new AuditionAdminException('Invalid number of scores');
}
foreach ($subscoresRequired as $subscore) {
// check that there is an element in the $scores collection with the key = $subscore->id
if (! $scores->keys()->contains($subscore->id)) {
throw new AuditionAdminException('Invalid Score Submission');
}
if ($scores[$subscore->id] > $subscore->max_score) {
throw new AuditionAdminException('Supplied subscore exceeds maximum allowed');
}
// Add subscore to the storage array
$subscoresStorageArray[$subscore->id] = [
'score' => $scores[$subscore->id],
'subscore_id' => $subscore->id,
'subscore_name' => $subscore->name,
];
// Multiply subscore by weight then add to total
$totalScore += ($subscore->weight * $scores[$subscore->id]);
$maxPossibleTotal += ($subscore->weight * $subscore->max_score);
}
$finalTotalScore = ($maxPossibleTotal === 0) ? 0 : (($totalScore / $maxPossibleTotal) * 100);
$entry->removeFlag('no_show');
if ($prelimScoreSheet instanceof PrelimScoreSheet) {
$prelimScoreSheet->update([
'user_id' => $user->id,
'entry_id' => $entry->id,
'subscores' => $subscoresStorageArray,
'total' => $finalTotalScore,
]);
} else {
$prelimScoreSheet = PrelimScoreSheet::create([
'user_id' => $user->id,
'entry_id' => $entry->id,
'subscores' => $subscoresStorageArray,
'total' => $finalTotalScore,
]);
}
// Log the prelim score entry
$log_message = 'Entered prelim score for entry id '.$entry->id.'.<br />';
$log_message .= 'Judge: '.$user->full_name().'<br />';
foreach ($prelimScoreSheet->subscores as $subscore) {
$log_message .= $subscore['subscore_name'].': '.$subscore['score'].'<br />';
}
$log_message .= 'Total :'.$prelimScoreSheet->total.'<br />';
auditionLog($log_message, [
'entries' => [$entry->id],
'users' => [$user->id],
'auditions' => [$entry->audition_id],
'students' => [$entry->student_id],
]);
// Check if we can make a status decision
$checker = app(CheckPrelimResult::class);
$checker($entry, true);
return $prelimScoreSheet;
}
}

View File

@ -6,121 +6,151 @@
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ScoreEntryException;
use App\Models\CalculatedScore;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\ScoreSheet;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use function auth;
class EnterScore
{
/**
* @param User $user A user acting as the judge for this sheet
* @param Entry $entry An entry to which this score should be assigned
* @param array $scores Scores to be entered in the form of SubscoreID => score
* @param ScoreSheet|false $scoreSheet If this is an update to an existing scoresheet, pass it here
* @return ScoreSheet The scoresheet that was created or updated
*
* @throws ScoreEntryException
*/
public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet
{
CalculatedScore::where('entry_id', $entry->id)->delete();
EntryTotalScore::where('entry_id', $entry->id)->delete();
$scores = collect($scores);
$this->basicChecks($user, $entry, $scores);
$this->checkJudgeAssignment($user, $entry);
$this->checkForExistingScore($user, $entry, $scoreSheet);
$this->validateScoresSubmitted($entry, $scores);
// Basic Validity Checks
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (! Entry::where('id', $entry->id)->exists()) {
throw new AuditionAdminException('Entry does not exist');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot score an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot score an entry in an audition where advancement is published');
}
// Check that the specified user is assigned to judge this entry
$check = DB::table('room_user')
->where('room_id', $entry->audition->room_id)
->where('user_id', $user->id)->exists();
if (! $check) {
throw new AuditionAdminException('This judge is not assigned to judge this entry');
}
// Check if a score already exists
if (! $scoreSheet) {
if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
throw new AuditionAdminException('That judge has already entered scores for that entry');
}
} else {
if ($scoreSheet->user_id !== $user->id) {
throw new AuditionAdminException('Existing score sheet is from a different judge');
}
if ($scoreSheet->entry_id !== $entry->id) {
throw new AuditionAdminException('Existing score sheet is for a different entry');
}
}
// Check the validity of submitted subscores, format array for storage, and sum score
$subscoresRequired = $entry->audition->scoringGuide->subscores;
$subscoresStorageArray = [];
$seatingTotal = 0;
$seatingMaxPossible = 0;
$advancementTotal = 0;
$advancementMaxPossible = 0;
if ($scores->count() !== $subscoresRequired->count()) {
throw new AuditionAdminException('Invalid number of scores');
}
foreach ($subscoresRequired as $subscore) {
// check that there is an element in the $scores collection with the key = $subscore->id
if (! $scores->keys()->contains($subscore->id)) {
throw new AuditionAdminException('Invalid Score Submission');
}
if ($scores[$subscore->id] > $subscore->max_score) {
throw new AuditionAdminException('Supplied subscore exceeds maximum allowed');
}
// Add subscore to the storage array
$subscoresStorageArray[$subscore->id] = [
'score' => $scores[$subscore->id],
'subscore_id' => $subscore->id,
'subscore_name' => $subscore->name,
];
// If included in seating, multiply by weight and add to the total and max possible
if ($subscore->for_seating) {
$seatingTotal += ($subscore->weight * $scores[$subscore->id]);
$seatingMaxPossible += ($subscore->weight * $subscore->max_score);
}
// If included in advancement, multiply by weight and add to the total and max possible
if ($subscore->for_advance) {
$advancementTotal += ($subscore->weight * $scores[$subscore->id]);
$advancementMaxPossible += ($subscore->weight * $subscore->max_score);
}
}
$finalSeatingTotal = ($seatingMaxPossible === 0) ? 0 : (($seatingTotal / $seatingMaxPossible) * 100);
$finalAdvancementTotal = ($advancementMaxPossible === 0) ? 0 : (($advancementTotal / $advancementMaxPossible) * 100);
$entry->removeFlag('no_show');
if ($scoreSheet instanceof ScoreSheet) {
$scoreSheet->update([
'user_id' => $user->id,
'entry_id' => $entry->id,
'subscores' => $this->subscoresForStorage($entry, $scores),
'subscores' => $subscoresStorageArray,
'seating_total' => $finalSeatingTotal,
'advancement_total' => $finalAdvancementTotal,
]);
} else {
$scoreSheet = ScoreSheet::create([
'user_id' => $user->id,
'entry_id' => $entry->id,
'subscores' => $this->subscoresForStorage($entry, $scores),
'subscores' => $subscoresStorageArray,
'seating_total' => $finalSeatingTotal,
'advancement_total' => $finalAdvancementTotal,
]);
}
// Log the score entry
$log_message = 'Entered Score for entry id '.$entry->id.'.<br />';
$log_message .= 'Judge: '.$user->full_name().'<br />';
foreach ($scoreSheet->subscores as $subscore) {
$log_message .= $subscore['subscore_name'].': '.$subscore['score'].'<br />';
}
$log_message .= 'Seating Total: '.$scoreSheet->seating_total.'<br />';
$log_message .= 'Advancement Total: '.$scoreSheet->advancement_total.'<br />';
AuditLogEntry::create([
'user' => auth()->user()->email ?? 'no user',
'ip_address' => request()->ip(),
'message' => $log_message,
'affected' => [
'entries' => [$entry->id],
'users' => [$user->id],
'auditions' => [$entry->audition_id],
'students' => [$entry->student_id],
],
]);
return $scoreSheet;
}
protected function subscoresForStorage(Entry $entry, Collection $scores)
{
$subscores = [];
foreach ($entry->audition->scoringGuide->subscores as $subscore) {
$subscores[$subscore->id] = [
'score' => $scores[$subscore->id],
'subscore_id' => $subscore->id,
'subscore_name' => $subscore->name,
];
}
return $subscores;
}
protected function checkForExistingScore(User $user, Entry $entry, $existingScoreSheet)
{
if (! $existingScoreSheet) {
if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
throw new ScoreEntryException('That judge has already entered scores for that entry');
}
} else {
if ($existingScoreSheet->user_id !== $user->id) {
throw new ScoreEntryException('Existing score sheet is from a different judge');
}
if ($existingScoreSheet->entry_id !== $entry->id) {
throw new ScoreEntryException('Existing score sheet is for a different entry');
}
}
}
protected function validateScoresSubmitted(Entry $entry, Collection $scores)
{
$subscoresRequired = $entry->audition->scoringGuide->subscores;
foreach ($subscoresRequired as $subscore) {
// check that there is an element in the $scores collection with the key = $subscore->id
if (! $scores->keys()->contains($subscore->id)) {
throw new ScoreEntryException('Invalid Score Submission');
}
if ($scores[$subscore->id] > $subscore->max_score) {
throw new ScoreEntryException('Supplied subscore exceeds maximum allowed');
}
}
}
protected function checkJudgeAssignment(User $user, Entry $entry)
{
$check = DB::table('room_user')
->where('room_id', $entry->audition->room_id)
->where('user_id', $user->id)->exists();
if (! $check) {
throw new ScoreEntryException('This judge is not assigned to judge this entry');
}
}
protected function basicChecks(User $user, Entry $entry, Collection $scores)
{
if (! $user->exists()) {
throw new ScoreEntryException('User does not exist');
}
if (! $entry->exists()) {
throw new ScoreEntryException('Entry does not exist');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new ScoreEntryException('Cannot score an entry in an audition with published seats');
}
if ($entry->audition->hasFlag('advancement_published')) {
throw new ScoreEntryException('Cannot score an entry in an audition with published advancement');
}
$requiredScores = $entry->audition->scoringGuide->subscores()->count();
if ($scores->count() !== $requiredScores) {
throw new ScoreEntryException('Invalid number of scores');
}
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Entry;
/**
* @codeCoverageIgnore
*/
class ForceRecalculateTotalScores
{
public function __invoke(): void
{
$calculator = app(TotalEntryScores::class);
foreach (Entry::all() as $entry) {
$calculator($entry, true);
}
}
}

View File

@ -2,11 +2,15 @@
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition;
use App\Models\Ensemble;
use App\Models\Seat;
use function dd;
/**
* @codeCoverageIgnore
*/
// TODO delete if truly depricated
class GetAuditionSeats
{
public function __construct()
@ -20,6 +24,7 @@ class GetAuditionSeats
protected function getSeats(Audition $audition)
{
throw new AuditionAdminException('This method is being considered for deletion.');
$ensembles = Ensemble::where('event_id', $audition->event_id)->orderBy('rank')->get();
$seats = Seat::with('student.school')->where('audition_id', $audition->id)->orderBy('seat')->get();
$return = [];

View File

@ -2,6 +2,7 @@
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition;
use App\Models\Seat;
use Illuminate\Support\Facades\Cache;
@ -13,8 +14,32 @@ class PublishSeats
//
}
/**
* Publishes the given audition with the provided seats.
*
* This method first validates that the seats array is not empty. If the array is empty,
* an AuditionAdminException is thrown.
*
* Next, it deletes existing records in the `seats` table associated with the provided audition
* using the `audition_id`.
*
* Then, it iterates through the provided seats array to create new records in the `seats` table
* with the specified `ensemble_id`, `audition_id`, `seat`, and `entry_id`.
*
* Finally, it marks the audition as having its seats published by adding a relevant flag
* to the audition, and clears cached data associated with the results seat list and
* public results page entries in the cache store.
*
* @param Audition $audition The audition instance to be published.
* @param array $seats An array of seat data to be associated with the audition.
*
* @throws AuditionAdminException If the provided seats array is empty.
*/
public function __invoke(Audition $audition, array $seats): void
{
if (count($seats) === 0) {
throw new AuditionAdminException('Cannot publish an audition with no seats.');
}
// Delete from the seats table where audition_id = $audition->id
Seat::where('audition_id', $audition->id)->delete();
foreach ($seats as $seat) {

View File

@ -4,105 +4,115 @@
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Cache;
use function is_numeric;
use App\Models\Entry;
use Illuminate\Support\Collection;
class RankAuditionEntries
{
protected CalculateEntryScore $calculator;
public function __construct(CalculateEntryScore $calculator)
{
$this->calculator = $calculator;
}
public function rank(string $mode, Audition $audition): Collection
{
$cacheKey = 'audition'.$audition->id.$mode;
return Cache::remember($cacheKey, 300, function () use ($mode, $audition) {
return $this->calculateRank($mode, $audition);
});
}
/**
* For a given audition, return a collection of entries ranked by total score. Each entry will have a
* property rank that either is their rank or a flag reflecting no-show, declined, or failed-prelim status
* Get ranked entries for the provided audition for either seating or advancement.
*
* @throws TabulationException
* If the rank_type is seating, the ranked entries are returned in descending order of seating total.
* If the rank_type is advancement, the ranked entries are returned in descending order of advancement total.
*
* The ranked entries are returned as a Collection of Entry objects.
*
* @param string $rank_type advancement|seating
* @return Collection<Entry>|void
*
* @throws AuditionAdminException
*/
public function calculateRank(string $mode, Audition $audition): Collection
public function __invoke(Audition $audition, string $rank_type, bool $pullDeclinedEntries = true): Collection|Entry
{
$this->basicValidation($mode, $audition);
$entries = match ($mode) {
'seating' => $audition->entries()->forSeating()->with('scoreSheets')->withCount('bonusScores')->get(),
'advancement' => $audition->entries()->forAdvancement()->with('scoreSheets')->get(),
};
foreach ($entries as $entry) {
$entry->setRelation('audition', $audition);
try {
$entry->score_totals = $this->calculator->calculate($mode, $entry);
} catch (TabulationException $ex) {
$entry->score_totals = [-1];
$entry->score_message = $ex->getMessage();
}
if ($rank_type !== 'seating' && $rank_type !== 'advancement') {
throw new AuditionAdminException('Invalid rank type (must be seating or advancement)');
}
// Sort entries based on their total score, then by subscores in tiebreak order
$entries = $entries->sort(function ($a, $b) {
for ($i = 0; $i < count($a->score_totals); $i++) {
if (! array_key_exists($i, $a->score_totals)) {
return -1;
}
if (! array_key_exists($i, $b->score_totals)) {
return -1;
}
if ($a->score_totals[$i] > $b->score_totals[$i]) {
return -1;
} elseif ($a->score_totals[$i] < $b->score_totals[$i]) {
return 1;
}
}
return 0;
$cache_duration = 15;
if ($rank_type === 'seating') {
return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition, $pullDeclinedEntries) {
return $this->get_seating_ranks($audition, $pullDeclinedEntries);
});
}
return cache()->remember('rank_advancement_'.$audition->id, $cache_duration, function () use ($audition) {
return $this->get_advancement_ranks($audition);
});
$rank = 1;
$rawRank = 1;
foreach ($entries as $entry) {
$entry->rank = $rank;
$entry->raw_rank = $rawRank;
// We don't really get a rank for seating if we have certain flags
if ($mode === 'seating') {
if ($entry->hasFlag('failed_prelim')) {
$entry->rank = 'Failed Prelim';
} elseif ($entry->hasFlag('declined')) {
$entry->rank = 'Declined';
} elseif ($entry->hasFlag('no_show')) {
$entry->rank = 'No Show';
}
}
if (is_numeric($entry->rank)) {
$rank++;
}
$rawRank++;
}
return $entries;
}
protected function basicValidation($mode, Audition $audition): void
private function get_seating_ranks(Audition $audition, bool $pullDeclinedEntries = true): Collection|Entry
{
if ($mode !== 'seating' && $mode !== 'advancement') {
throw new TabulationException('Mode must be seating or advancement');
if ($audition->bonusScore()->count() > 0) {
$totalColumn = 'seating_total_with_bonus';
} else {
$totalColumn = 'seating_total';
}
if (! $audition->exists()) {
throw new TabulationException('Invalid audition provided');
$sortedEntries = $audition->entries()
->where('for_seating', true)
->whereHas('totalScore')
->with('totalScore')
->with('student.school')
->with('audition')
->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id')
->orderBy('entry_total_scores.'.$totalColumn, 'desc')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[0]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[1]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[2]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[3]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[4]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[5]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[6]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[7]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[8]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[9]"), -999999) DESC')
->select('entries.*')
->get();
$rankOn = 1;
foreach ($sortedEntries as $entry) {
if ($entry->hasFlag('declined') && $pullDeclinedEntries) {
$entry->seatingRank = 'declined';
} else {
$entry->seatingRank = $rankOn;
$rankOn++;
}
}
return $sortedEntries;
}
private function get_advancement_ranks(Audition $audition): Collection|Entry
{
$sortedEntries = $audition->entries()
->whereHas('totalScore')
->with('totalScore')
->with('student.school')
->with('audition')
->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id')
->orderBy('entry_total_scores.advancement_total', 'desc')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[0]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[1]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[2]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[3]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[4]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[5]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[6]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[7]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[8]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC')
->select('entries.*')
->get();
$n = 1;
foreach ($sortedEntries as $entry) {
$entry->advancementRank = $n;
$n++;
}
return $sortedEntries;
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\BonusScore;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\ScoreSheet;
/**
* Handles the calculation of a total score for an entry, including seating and advancement scores,
* based on scoring sheets and subscores defined in the audition's scoring guide.
*/
class TotalEntryScores
{
public function __construct()
{
}
public function __invoke(Entry $entry, bool $force_recalculation = false): void
{
if ($force_recalculation) {
EntryTotalScore::where('entry_id', $entry->id)->delete();
}
// bail out if a total score is already calculated
if (EntryTotalScore::where('entry_id', $entry->id)->count() > 0) {
return;
}
$requiredSubscores = $entry->audition->scoringGuide->subscores;
$newTotaledScore = EntryTotalScore::make();
$newTotaledScore->entry_id = $entry->id;
// deal with seating scores
// TODO: Consider a rewrite to pull the scoreSheets from the entry model so they may be preloaded
$scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('seating_total', 'desc')->get();
// bail out if there are not enough score sheets
$assignedJudges = $entry->audition->judges()->count();
if ($scoreSheets->count() == 0 || $scoreSheets->count() < $assignedJudges) {
return;
}
if (auditionSetting('olympic_scoring') && $scoreSheets->count() > 2) {
// under olympic scoring, drop the first and last element
$scoreSheets->shift();
$scoreSheets->pop();
}
$newTotaledScore->seating_total = round($scoreSheets->avg('seating_total'), 6);
$seatingSubscores = $requiredSubscores
->filter(fn ($subscore) => $subscore->for_seating == true)
->sortBy('tiebreak_order');
$total_seating_subscores = [];
foreach ($seatingSubscores as $subscore) {
$runningTotal = 0;
foreach ($scoreSheets as $scoreSheet) {
$runningTotal += $scoreSheet->subscores[$subscore->id]['score'];
}
$total_seating_subscores[] = round($runningTotal / $scoreSheets->count(), 4);
}
$newTotaledScore->seating_subscore_totals = $total_seating_subscores;
// deal with advancement scores
$scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('advancement_total', 'desc')->get();
if (auditionSetting('olympic_scoring') && $scoreSheets->count() > 2) {
// under olympic scoring, drop the first and last element
$scoreSheets->shift();
$scoreSheets->pop();
}
$newTotaledScore->advancement_total = round($scoreSheets->avg('advancement_total'), 6);
$advancement_subscores = $requiredSubscores
->filter(fn ($subscore) => $subscore->for_advance == true)
->sortBy('tiebreak_order');
$total_advancement_subscores = [];
foreach ($advancement_subscores as $subscore) {
$runningTotal = 0;
foreach ($scoreSheets as $scoreSheet) {
$runningTotal += $scoreSheet->subscores[$subscore->id]['score'];
}
$total_advancement_subscores[] = round($runningTotal / $scoreSheets->count(), 4);
}
$newTotaledScore->advancement_subscore_totals = $total_advancement_subscores;
// pull in bonus scores
$bonusScores = BonusScore::where('entry_id', $entry->id)
->selectRaw('SUM(score) as total')
->value('total');
$newTotaledScore->bonus_total = $bonusScores;
$newTotaledScore->save();
}
}

View File

@ -9,10 +9,6 @@ use Carbon\Carbon;
class RecordHistoricalSeats
{
public function __construct()
{
}
public function __invoke(): void
{
$this->saveSeats();

View File

@ -6,24 +6,26 @@ use App\Exceptions\AuditionAdminException;
use App\Models\AuditionFlag;
use App\Models\AuditLogEntry;
use App\Models\BonusScore;
use App\Models\CalculatedScore;
use App\Models\Doubler;
use App\Models\DoublerRequest;
use App\Models\EntryFlag;
use App\Models\EntryTotalScore;
use App\Models\JudgeAdvancementVote;
use App\Models\NominationEnsembleEntry;
use App\Models\ScoreSheet;
use App\Models\Seat;
use App\Models\Student;
use App\Models\UserFlag;
use Illuminate\Support\Facades\DB;
use function auth;
/**
* @codeCoverageIgnore
*/
// TODO: figure out how to test YearEndCleanup
class YearEndCleanup
{
public function __construct()
{
}
public function __invoke(?array $options = []): void
{
$this->cleanup($options);
@ -49,8 +51,9 @@ class YearEndCleanup
AuditLogEntry::truncate();
AuditionFlag::truncate();
BonusScore::truncate();
CalculatedScore::truncate();
EntryTotalScore::truncate();
DoublerRequest::truncate();
Doubler::truncate();
EntryFlag::truncate();
ScoreSheet::truncate();
Seat::truncate();
@ -62,19 +65,20 @@ class YearEndCleanup
if (is_array($options)) {
if (in_array('deleteRooms', $options)) {
DB::table('auditions')->update(['room_id' => null]);
DB::table('auditions')->update(['room_id' => 0]);
DB::table('auditions')->update(['order_in_room' => '0']);
DB::table('room_user')->truncate();
DB::table('rooms')->delete();
DB::table('rooms')->where('id', '>', 0)->delete();
}
if (in_array('removeAuditionsFromRoom', $options)) {
DB::table('auditions')->update(['room_id' => null]);
DB::table('auditions')->update(['room_id' => 0]);
DB::table('auditions')->update(['order_in_room' => '0']);
}
if (in_array('unassignJudges', $options)) {
DB::table('room_user')->truncate();
UserFlag::where('flag', 'monitor')->delete();
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use App\Actions\Tabulation\EnterScore;
use App\Models\ScoreSheet;
use Illuminate\Console\Command;
class RecalculateJudgeTotalsCommand extends Command
{
protected $signature = 'audition:recalculate-judge-totals';
protected $description = 'Recalculates total scores for all score sheets for unpubished auditions';
public function handle(): void
{
$this->info('Starting score recalculation...');
$scoreSheets = ScoreSheet::all();
foreach ($scoreSheets as $scoreSheet) {
if ($scoreSheet->entry->audition->hasFlag('seats_published')) {
continue;
}
$this->recalculate($scoreSheet);
}
$this->info('Score recalculation completed successfully.');
}
private function recalculate(ScoreSheet|int $scoreSheet): void
{
if (is_int($scoreSheet)) {
$scoreSheet = ScoreSheet::findOrFail($scoreSheet);
}
$scribe = app()->make(EnterScore::class);
$scoreSubmission = [];
foreach ($scoreSheet->subscores as $subscore) {
$scoreSubmission[$subscore['subscore_id']] = $subscore['score'];
}
$scribe($scoreSheet->judge, $scoreSheet->entry, $scoreSubmission, $scoreSheet);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands;
use App\Actions\Tabulation\ForceRecalculateTotalScores;
use Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class RecalculateTotalScores extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:recalculate-total-scores';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Forces the recalculation of total scores for all entries';
/**
* Execute the console command.
*/
public function handle(ForceRecalculateTotalScores $action): void
{
$this->info('Starting score recalculation...');
$action();
$this->info('Score recalculation completed successfully.');
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Console\Commands;
use App\Actions\Tabulation\DoublerSync;
use App\Models\Event;
use Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class SyncDoublers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:sync-doublers {event? : Optional event ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update doublers table based on current entries';
/**
* Execute the console command.
*/
public function handle()
{
$syncer = app(DoublerSync::class);
if ($eventId = $this->argument('event')) {
$event = Event::findOrFail($eventId);
$syncer($event);
$this->info("Synced doublers for event {$event->name}");
} else {
$syncer();
$this->info('Synced doublers for all events');
}
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Console\Commands;
use App\Models\School;
use App\Models\Student;
use App\Models\User;
use Faker\Factory;
use Illuminate\Console\Command;
class fictionalize extends Command
{
protected $signature = 'audition:fictionalize
{--students : Fictionalize student names}
{--schools : Fictionalize school names}
{--users : Fictionalize user data}
{--all : Fictionalize all data types}';
protected $description = 'Replace real names with fictional data for specified entity types';
public function handle()
{
$faker = Factory::create();
// If no options are specified or --all is used, process everything
$processAll = $this->option('all') ||
(! $this->option('students') && ! $this->option('schools') && ! $this->option('users'));
if ($processAll || $this->option('students')) {
$this->info('Fictionalizing students...');
$bar = $this->output->createProgressBar(Student::count());
Student::chunk(100, function ($students) use ($faker, $bar) {
foreach ($students as $student) {
$student->update([
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
if ($processAll || $this->option('schools')) {
$this->info('Fictionalizing schools...');
$bar = $this->output->createProgressBar(School::count());
School::chunk(100, function ($schools) use ($faker, $bar) {
foreach ($schools as $school) {
$school->update([
'name' => $faker->city().' High School',
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
if ($processAll || $this->option('users')) {
$this->info('Fictionalizing users...');
$bar = $this->output->createProgressBar(User::where('email', '!=', 'matt@mattyoung.us')->count());
User::where('email', '!=', 'matt@mattyoung.us')
->chunk(100, function ($users) use ($faker, $bar) {
foreach ($users as $user) {
$user->update([
'email' => $faker->unique()->email(),
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
'cell_phone' => $faker->phoneNumber(),
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
$this->info('Fictionalization complete!');
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace App\Console\Commands;
use App\Models\Audition;
use App\Models\Event;
use App\Models\Room;
use App\Models\ScoringGuide;
use App\Services\CsvImportService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use function auditionSetting;
use function Laravel\Prompts\select;
class importCheckAuditionsCommand extends Command
{
protected $signature = 'import:check-auditions';
protected $description = 'Check the import file for auditions that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$lowestPossibleGrade = 1;
$highestPossibleGrade = 12;
$events = Event::all();
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedAuditions = collect();
foreach ($rows as $row) {
if ($checkedAuditions->contains($row['Instrument'])) {
continue;
}
$checkedAuditions->push($row['Instrument']);
if (Audition::where('name', $row['Instrument'])->count() > 0) {
$this->info('Audition '.$row['Instrument'].' already exists');
} else {
$this->newLine();
$this->alert('Audition '.$row['Instrument'].' does not exist');
if ($events->count() === 1) {
$newEventId = $events->first()->id;
} else {
$newEventId = select(
label: 'Which event does this audition belong to?',
options: $events->pluck('name', 'id')->toArray(),
);
}
$newEventName = $row['Instrument'];
$newEventScoreOrder = Audition::max('score_order') + 1;
$newEventEntryDeadline = Carbon::yesterday('America/Chicago')->format('Y-m-d');
$newEventEntryFee = Audition::max('entry_fee');
$newEventMinimumGrade = select(
label: 'What is the minimum grade for this audition?',
options: range($lowestPossibleGrade, $highestPossibleGrade)
);
$newEventMaximumGrade = select(
label: 'What is the maximum grade for this audition?',
options: range($newEventMinimumGrade, $highestPossibleGrade)
);
$newEventRoomId = select(
label: 'Which room does this audition belong to?',
options: Room::pluck('name', 'id')->toArray(),
);
$newEventScoringGuideId = select(
label: 'Which scoring guide should this audition use',
options: ScoringGuide::pluck('name', 'id')->toArray(),
);
if (auditionSetting('advanceTo')) {
$newEventForSeating = select(
label: 'Is this audition for seating?',
options: [
1 => 'Yes',
0 => 'No',
]
);
$newEventForAdvance = select(
label: 'Is this audition for '.auditionSetting('advanceTo').'?',
options: [
1 => 'Yes',
0 => 'No',
]
);
} else {
$newEventForSeating = 1;
$newEventForAdvance = 0;
}
$this->info('New event ID: '.$newEventId);
$this->info('New event name: '.$newEventName);
$this->info('New event score order: '.$newEventScoreOrder);
$this->info('New event entry deadline: '.$newEventEntryDeadline);
$this->info('New event entry fee: '.$newEventEntryFee);
$this->info('New event minimum grade: '.$newEventMinimumGrade);
$this->info('New event maximum grade: '.$newEventMaximumGrade);
$this->info('New event room ID: '.$newEventRoomId);
$this->info('New event scoring guide ID: '.$newEventScoringGuideId);
$this->info('New event for seating: '.$newEventForSeating);
$this->info('New event for advance: '.$newEventForAdvance);
Audition::create([
'event_id' => $newEventId,
'name' => $newEventName,
'score_order' => $newEventScoreOrder,
'entry_deadline' => $newEventEntryDeadline,
'entry_fee' => $newEventEntryFee,
'minimum_grade' => $newEventMinimumGrade,
'maximum_grade' => $newEventMaximumGrade,
'room_id' => $newEventRoomId,
'scoring_guide_id' => $newEventScoringGuideId,
'for_seating' => $newEventForSeating,
'for_advancement' => $newEventForAdvance,
]);
}
}
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use const PHP_EOL;
use App\Models\School;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importCheckSchoolsCommand extends Command
{
protected $signature = 'import:check-schools';
protected $description = 'Check the import file for schools that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedSchools = collect();
foreach ($rows as $row) {
if ($checkedSchools->contains($row['School'])) {
continue;
}
$checkedSchools->push($row['School']);
if (School::where('name', $row['School'])->count() > 0) {
$this->info('School '.$row['School'].' already exists');
} else {
$this->newLine();
$this->alert('School '.$row['School'].' does not exist'.PHP_EOL.'Creating school...');
School::create(['name' => $row['School']]);
$this->info('School '.$row['School'].' created');
}
}
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Console\Commands;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importCheckStudentsCommand extends Command
{
protected $signature = 'import:check-students';
protected $description = 'Check the import file for students that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$purge = $this->confirm('Do you want to purge the database of existing students and entries?', false);
if ($purge) {
Entry::all()->map(function ($entry) {
$entry->delete();
});
Student::all()->map(function ($student) {
$student->delete();
});
$this->info('Database purged');
}
$schools = School::pluck('id', 'name');
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedStudents = collect();
foreach ($rows as $row) {
$uniqueData = $row['School'].$row['LastName'].$row['LastName'];
if ($checkedStudents->contains($uniqueData)) {
// continue;
}
$checkedStudents->push($uniqueData);
$currentFirstName = $row['FirstName'];
$currentLastName = $row['LastName'];
$currentSchoolName = $row['School'];
$currentSchoolId = $schools[$currentSchoolName];
if (Student::where('first_name', $currentFirstName)->where('last_name',
$currentLastName)->where('school_id', $currentSchoolId)->count() > 0) {
$this->info('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' already exists');
} else {
$this->alert('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' does not exist');
$newStudent = Student::create([
'school_id' => $currentSchoolId,
'first_name' => $currentFirstName,
'last_name' => $currentLastName,
'grade' => $row['Grade'],
]);
$this->info('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' created with id of: '.$newStudent->id);
}
}
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importImportEntriesCommand extends Command
{
protected $signature = 'import';
protected $description = 'Import entries from the import.csv file. First check schools, then students, then auditions, then run this import command';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$checkAuditions = $this->confirm('Do you want to check the auditions in the import for validity first?', true);
if ($checkAuditions) {
$this->call('import:check-auditions');
}
$checkSchools = $this->confirm('Do you want to check the schools in the import for validity first?', true);
if ($checkSchools) {
$this->call('import:check-schools');
}
$checkStudents = $this->confirm('Do you want to check the students in the import for validity first?', true);
if ($checkStudents) {
$this->call('import:check-students');
}
$purge = $this->confirm('Do you want to purge the database of existing entries?', false);
if ($purge) {
Entry::all()->map(function ($entry) {
$entry->delete();
});
$this->info('Database purged');
}
$schools = School::pluck('id', 'name');
$auditions = Audition::pluck('id', 'name');
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
foreach ($rows as $row) {
$schoolId = $schools[$row['School']];
$student = Student::where('first_name', $row['FirstName'])->where('last_name',
$row['LastName'])->where('school_id', $schoolId)->first();
if (! $student) {
$this->error('Student '.$row['FirstName'].' '.$row['LastName'].' from '.$row['School'].' does not exist');
return;
}
$auditionId = $auditions[$row['Instrument']];
try {
Entry::create([
'student_id' => $student->id,
'audition_id' => $auditionId,
]);
} catch (\Exception $e) {
$this->warn('Entry already exists for student '.$student->full_name().' in audition '.$row['Instrument']);
}
$this->info('Entry created for student '.$student->full_name().' in audition '.$row['Instrument']);
}
}
}

View File

@ -8,5 +8,6 @@ enum EntryFlags: string
case DECLINED = 'declined';
case NO_SHOW = 'no_show';
case FAILED_PRELIM = 'failed_prelim';
case PASSED_PRELIM = 'passed_prelim';
case LATE_FEE_WAIVED = 'late_fee_waived';
}

View File

@ -6,5 +6,5 @@ use Exception;
class AuditionServiceException extends Exception
{
//
//TODO: Fully depricate this class
}

View File

@ -1,19 +1,24 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use App\Exceptions\TabulationException;
/**
* @codeCoverageIgnore
*/
//TODO: Fully depricate this class
class Handler extends ExceptionHandler
{
public function render($request, Throwable $e)
{
if ($e instanceof TabulationException) {
dd('here');
return redirect('/tabulation/status')->with('warning', $e->getMessage());
}
return parent::render($request, $e);
}
}

View File

@ -6,4 +6,5 @@ use Exception;
class ManageEntryException extends Exception
{
//TODO: Fully depricate this class
}

View File

@ -6,5 +6,5 @@ use Exception;
class ScoreEntryException extends Exception
{
//
//TODO: Fully depricate this class
}

View File

@ -3,20 +3,24 @@
namespace App\Exceptions;
use Exception;
use Throwable;
use function dd;
use function redirect;
/**
* @codeCoverageIgnore
*/
class TabulationException extends Exception
{
public function report(): void
{
//
//TODO: Fully depricate this class
}
public function render($request)
{
dd('in the render');
return redirect('/tabulation/status')->with('error', $this->getMessage());
return redirect('/tabulation/status')->with('error', $this->getMessage());
}
}

View File

@ -3,13 +3,15 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\AuditionStoreOrUpdateRequest;
use App\Http\Requests\BulkAuditionEditRequest;
use App\Models\Audition;
use App\Models\Event;
use App\Models\Room;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function abort;
use function compact;
use function redirect;
use function request;
use function response;
@ -28,38 +30,20 @@ class AuditionController extends Controller
public function create()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$events = Event::orderBy('name')->get();
return view('admin.auditions.create', ['events' => $events]);
}
public function store(Request $request)
public function store(AuditionStoreOrUpdateRequest $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = request()->validate([
'event_id' => ['required', 'exists:events,id'],
'name' => ['required'],
'entry_deadline' => ['required', 'date'],
'entry_fee' => ['required', 'numeric'],
'minimum_grade' => ['required', 'integer'],
'maximum_grade' => 'required|numeric|gte:minimum_grade',
'scoring_guide_id' => 'nullable|exists:scoring_guides,id',
], [
'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.',
]);
$validData = $request->validated();
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
if (empty($alidData['scoring_guide_id'])) {
if (empty($validData['scoring_guide_id'])) {
$validData['scoring_guide_id'] = 0;
}
$new_score_order = Audition::max('score_order') + 1;
// TODO Check if room 0 exists, create if not
$validData['score_order'] = Audition::max('score_order') + 1;
Audition::create([
'event_id' => $validData['event_id'],
'name' => $validData['name'],
@ -71,7 +55,7 @@ class AuditionController extends Controller
'for_advancement' => $validData['for_advancement'],
'scoring_guide_id' => $validData['scoring_guide_id'],
'room_id' => 0,
'score_order' => $new_score_order,
'score_order' => $validData['score_order'],
]);
return to_route('admin.auditions.index')->with('success', 'Audition created successfully');
@ -79,33 +63,14 @@ class AuditionController extends Controller
public function edit(Audition $audition)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$events = Event::orderBy('name')->get();
return view('admin.auditions.edit', ['audition' => $audition, 'events' => $events]);
}
public function update(Request $request, Audition $audition)
public function update(AuditionStoreOrUpdateRequest $request, Audition $audition)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = request()->validate([
'event_id' => ['required', 'exists:events,id'],
'name' => ['required'],
'entry_deadline' => ['required', 'date'],
'entry_fee' => ['required', 'numeric'],
'minimum_grade' => ['required', 'integer'],
'maximum_grade' => 'required | numeric | gte:minimum_grade',
], [
'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.',
]);
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
$validData = $request->validated();
$audition->update([
'event_id' => $validData['event_id'],
@ -121,11 +86,59 @@ class AuditionController extends Controller
return to_route('admin.auditions.index')->with('success', 'Audition updated successfully');
}
public function bulkEditForm()
{
$auditions = Audition::with(['event'])->withCount('entries')->orderBy('score_order')->orderBy('created_at',
'desc')->get()->groupBy('event_id');
$events = Event::orderBy('name')->get();
return view('admin.auditions.bulk_edit_form', compact('auditions', 'events'));
}
public function bulkUpdate(BulkAuditionEditRequest $request)
{
$validated = collect($request->validated());
$auditions = Audition::whereIn('id', $validated['auditions'])->get();
foreach ($auditions as $audition) {
if ($validated->has('event_id')) {
$audition->event_id = $validated['event_id'];
}
if ($validated->has('entry_deadline')) {
$audition->entry_deadline = $validated['entry_deadline'];
}
if ($validated->has('entry_fee')) {
$audition->entry_fee = $validated['entry_fee'];
}
if ($validated->has('minimum_grade')) {
$originalMinimumGrade = $audition->minimum_grade;
$audition->minimum_grade = $validated['minimum_grade'];
}
if ($validated->has('maximum_grade')) {
$originalMaximumGrade = $audition->maximum_grade;
$audition->maximum_grade = $validated['maximum_grade'];
}
if ($validated->has('for_seating')) {
$audition->for_seating = $validated['for_seating'];
}
if ($validated->has('for_advancement')) {
$audition->for_advancement = $validated['for_advancement'];
}
if ($audition->minimum_grade > $audition->maximum_grade) {
$audition->minimum_grade = $originalMinimumGrade;
$audition->maximum_grade = $originalMaximumGrade;
}
$audition->save();
}
return to_route('admin.auditions.index')->with('success', $auditions->count().' Auditions updated successfully');
}
public function reorder(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order;
foreach ($order as $index => $id) {
$audition = Audition::find($id);
@ -138,9 +151,15 @@ class AuditionController extends Controller
public function roomUpdate(Request $request)
{
$auditions = $request->all();
/**
* $auditions will be an array of arrays with the following structure:
* [
* ['id' => 1, 'room_id' => 1, 'room_order' => 1],
* ]
* is is an audition id
*/
foreach ($auditions as $audition) {
Audition::where('id', $audition['id'])
$a = Audition::where('id', $audition['id'])
->update([
'room_id' => $audition['room_id'],
'order_in_room' => $audition['room_order'],

View File

@ -16,15 +16,17 @@ class AuditionSettings extends Controller
return view('admin.audition-settings');
}
/** @codeCoverageIgnore */
public function save(Request $request)
{
// TODO update validation rules to match the settings table
$validData = $request->validate([
'auditionName' => ['required'],
'auditionAbbreviation' => ['required', 'max:10'],
'organizerName' => ['required'],
'organizerEmail' => ['required', 'email'],
'registrationCode' => ['required'],
'fee_structure' => ['required', 'in:oneFeePerEntry,oneFeePerStudent'],
'fee_structure' => ['required', 'in:oneFeePerEntry,oneFeePerStudent,oneFeePerStudentPerEvent'],
// Options should align with the boot method of InvoiceDataServiceProvider
'late_fee' => ['nullable', 'numeric', 'min:0'],
'school_fee' => ['nullable', 'numeric', 'min:0'],

View File

@ -27,7 +27,7 @@ class BonusScoreDefinitionController extends Controller
public function store()
{
$validData = request()->validate([
'name' => 'required',
'name' => 'required|unique:bonus_score_definitions,name',
'max_score' => 'required|numeric',
'weight' => 'required|numeric',
]);
@ -37,6 +37,20 @@ class BonusScoreDefinitionController extends Controller
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Created');
}
public function update(BonusScoreDefinition $bonusScore)
{
$validData = request()->validate([
'name' => 'required|unique:bonus_score_definitions,name,'.$bonusScore->id,
'max_score' => 'required|numeric',
'weight' => 'required|numeric',
]);
$bonusScore->update($validData);
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Updated');
}
public function destroy(BonusScoreDefinition $bonusScore)
{
if ($bonusScore->auditions()->count() > 0) {
@ -49,6 +63,7 @@ class BonusScoreDefinitionController extends Controller
public function assignAuditions(Request $request)
{
// TODO: add pivot model to log changes to assignments
$validData = $request->validate([
'bonus_score_id' => 'required|exists:bonus_score_definitions,id',
'audition' => 'required|array',
@ -70,12 +85,8 @@ class BonusScoreDefinitionController extends Controller
public function unassignAudition(Audition $audition)
{
if (! $audition->exists()) {
return redirect()->route('admin.bonus-scores.index')->with('error', 'Audition not found');
}
if (! $audition->bonusScore()->count() > 0) {
return redirect()->route('admin.bonus-scores.index')->with('error', 'Audition does not have a bonus score');
}
// TODO: add pivot model to log changes to assignments
$audition->bonusScore()->detach();
return redirect()->route('admin.bonus-scores.index')->with('success', 'Audition unassigned from bonus score');
@ -83,6 +94,7 @@ class BonusScoreDefinitionController extends Controller
public function judges()
{
//TODO Need to show if judge is assigned, and show bonus assignments or normal judging page
$bonusScores = BonusScoreDefinition::all();
$users = User::orderBy('last_name')->orderBy('first_name')->get();
@ -91,9 +103,6 @@ class BonusScoreDefinitionController extends Controller
public function assignJudge(BonusScoreDefinition $bonusScore)
{
if (! $bonusScore->exists()) {
return redirect()->route('admin.bonus-scores.judges')->with('error', 'Bonus Score not found');
}
$validData = request()->validate([
'judge' => 'required|exists:users,id',
]);
@ -104,9 +113,6 @@ class BonusScoreDefinitionController extends Controller
public function removeJudge(BonusScoreDefinition $bonusScore)
{
if (! $bonusScore->exists()) {
return redirect()->route('admin.bonus-scores.judges')->with('error', 'Bonus Score not found');
}
$validData = request()->validate([
'judge' => 'required|exists:users,id',
]);

View File

@ -2,30 +2,24 @@
namespace App\Http\Controllers\Admin;
use App\Actions\Draw\ClearDraw;
use App\Actions\Draw\RunDraw;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClearDrawRequest;
use App\Http\Requests\RunDrawRequest;
use App\Models\Audition;
use App\Models\Event;
use App\Services\DrawService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use function array_keys;
use function to_route;
class DrawController extends Controller
{
protected $drawService;
public function __construct(DrawService $drawService)
{
$this->drawService = $drawService;
}
public function index(Request $request)
{
$events = Event::with('auditions.flags')->get();
// $drawnAuditionsExist is true if any audition->hasFlag('drawn') is true
$drawnAuditionsExist = Audition::whereHas('flags', function ($query) {
$query->where('flag_name', 'drawn');
@ -36,18 +30,23 @@ class DrawController extends Controller
public function store(RunDrawRequest $request)
{
// Request will contain audition which is an array of audition IDs all with a value of 1
// Code below results in a collection of auditions that were checked on the form
$auditions = Audition::with('flags')->findMany(array_keys($request->input('audition', [])));
if ($this->drawService->checkCollectionForDrawnAuditions($auditions)) {
if ($auditions->contains(fn ($audition) => $audition->hasFlag('drawn'))) {
return to_route('admin.draw.index')->with('error',
'Invalid attempt to draw an audition that has already been drawn');
'Cannot run draw. Some auditions have already been drawn.');
}
$this->drawService->runDrawsOnCollection($auditions);
app(RunDraw::class)($auditions);
return to_route('admin.draw.index')->with('status', 'Draw completed successfully');
return to_route('admin.draw.index')->with('success', 'Draw completed successfully');
}
/**
* generates the page with checkboxes for each drawn audition with an intent to clear them
*/
public function edit(Request $request)
{
$drawnAuditions = Audition::whereHas('flags', function ($query) {
@ -57,12 +56,17 @@ class DrawController extends Controller
return view('admin.draw.edit', compact('drawnAuditions'));
}
/**
* Clears the draw for auditions
*/
public function destroy(ClearDrawRequest $request)
{
// Request will contain audition which is an array of audition IDs all with a value of 1
// Code below results in a collection of auditions that were checked on the form
$auditions = Audition::with('flags')->findMany(array_keys($request->input('audition', [])));
$this->drawService->clearDrawsOnCollection($auditions);
app(ClearDraw::class)($auditions);
return to_route('admin.draw.index')->with('status', 'Draw completed successfully');
return to_route('admin.draw.index')->with('success', 'Draws cleared successfully');
}
}

View File

@ -3,12 +3,13 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\EnsembleStoreOrUpdateRequest;
use App\Models\Ensemble;
use App\Models\Event;
use App\Models\SeatingLimit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use function redirect;
@ -21,30 +22,24 @@ class EnsembleController extends Controller
return view('admin.ensembles.index', compact('events'));
}
public function store(Request $request)
public function store(EnsembleStoreOrUpdateRequest $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'name' => 'required',
'code' => ['required', 'max:6'],
'event_id' => ['required', 'exists:events,id'],
]);
// get the maximum value of rank from the ensembles table where event_id is equal to the request event_id
Log::channel('file')->warning('hello');
$validated = $request->validated();
// get the maximum value of rank from the ensemble table where event_id is equal to the request event_id
$maxCode = Ensemble::where('event_id', request('event_id'))->max('rank');
Ensemble::create([
'name' => request('name'),
'code' => request('code'),
'event_id' => request('event_id'),
'name' => $validated['name'],
'code' => $validated['code'],
'event_id' => $validated['event_id'],
'rank' => $maxCode + 1,
]);
return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble created successfully');
}
public function destroy(Request $request, Ensemble $ensemble)
public function destroy(Ensemble $ensemble)
{
if ($ensemble->seats->count() > 0) {
return redirect()->route('admin.ensembles.index')->with('error',
@ -55,25 +50,32 @@ class EnsembleController extends Controller
return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble deleted successfully');
}
public function updateEnsemble(Request $request, Ensemble $ensemble)
public function update(EnsembleStoreOrUpdateRequest $request, Ensemble $ensemble)
{
request()->validate([
'name' => 'required',
'code' => 'required|max:6',
]);
$valid = $request->validated();
$ensemble->update([
'name' => request('name'),
'code' => request('code'),
'name' => $valid['name'],
'code' => $valid['code'],
]);
return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble updated successfully');
}
//TODO Consider moving seating limit related functions to their own controller with index, edit, and update methods
public function seatingLimits(Ensemble $ensemble)
{
$limits = [];
$ensembles = Ensemble::with(['event'])->orderBy('event_id')->get();
/**
* If we weren't called with an ensemble, we're going to use an array of ensembles to fill a drop-down and
* choose one. The user will be sent back here, this time with the chosen audition.
*/
$ensembles = Ensemble::with(['event'])->orderBy('event_id')->orderBy('rank')->get();
/**
* If we were called with an ensemble, we need to load existing seating limits. We will put them in an array
* indexed by audition_id for easy use in the form to set seating limits.
*/
if ($ensemble->exists()) {
$ensemble->load('seatingLimits');
foreach ($ensemble->seatingLimits as $lim) {
@ -112,10 +114,6 @@ class EnsembleController extends Controller
public function updateEnsembleRank(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->input('order');
$eventId = $request->input('event_id');

View File

@ -4,18 +4,15 @@ namespace App\Http\Controllers\Admin;
use App\Actions\Entries\CreateEntry;
use App\Actions\Entries\UpdateEntry;
use App\Actions\Tabulation\CalculateScoreSheetTotal;
use App\Exceptions\ManageEntryException;
use App\Http\Controllers\Controller;
use App\Http\Requests\EntryStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\School;
use App\Models\Seat;
use App\Models\Student;
use App\Services\ScoreService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function auditionSetting;
use function compact;
@ -25,9 +22,6 @@ class EntryController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$perPage = 25;
$filters = session('adminEntryFilters') ?? null;
$minGrade = Audition::min('minimum_grade');
@ -38,31 +32,31 @@ class EntryController extends Controller
$entries = Entry::with(['student.school', 'audition']);
$entries->orderBy('id', 'DESC');
if ($filters) {
if ($filters['id']) {
if ($filters['id'] ?? false) {
$entries->where('id', $filters['id']);
}
if ($filters['audition']) {
if ($filters['audition'] ?? false) {
$entries->where('audition_id', $filters['audition']);
}
if ($filters['school']) {
if ($filters['school'] ?? false) {
$entries->whereHas('student', function ($query) use ($filters) {
$query->where('school_id', '=', $filters['school']);
});
}
if ($filters['grade']) {
if ($filters['grade'] ?? false) {
$entries->whereHas('student', function ($query) use ($filters) {
$query->where('grade', $filters['grade']);
});
}
if ($filters['first_name']) {
if ($filters['first_name'] ?? false) {
$entries->whereHas('student', function ($query) use ($filters) {
$query->where('first_name', 'like', '%'.$filters['first_name'].'%');
});
}
if ($filters['last_name']) {
if ($filters['last_name'] ?? false) {
$entries->whereHas('student', function ($query) use ($filters) {
$query->where('last_name', 'like', '%'.$filters['last_name'].'%');
});
@ -71,7 +65,6 @@ class EntryController extends Controller
if (isset($filters['entry_type']) && $filters['entry_type']) {
// TODO define actions for each possible type filter from index.blade.php of the admin entry
match ($filters['entry_type']) {
'all' => null,
'seats' => $entries->where('for_seating', true),
'advancement' => $entries->where('for_advancement', true),
'seatsOnly' => $entries->where('for_seating', true)->where('for_advancement', false),
@ -110,32 +103,19 @@ class EntryController extends Controller
return view('admin.entries.create', ['students' => $students, 'auditions' => $auditions]);
}
public function store(Request $request, CreateEntry $creator)
public function store(EntryStoreRequest $request, CreateEntry $creator)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = request()->validate([
'student_id' => ['required', 'exists:students,id'],
'audition_id' => ['required', 'exists:auditions,id'],
]);
$validData = $request->validatedWithEnterFor();
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
$validData['late_fee_waived'] = $request->get('late_fee_waived') ? 1 : 0;
$enter_for = [];
if ($validData['for_seating']) {
$enter_for[] = 'seating';
}
if ($validData['for_advancement']) {
$enter_for[] = 'advancement';
}
/** @noinspection PhpUnhandledExceptionInspection */
$entry = $creator(
student: $validData['student_id'],
audition: $validData['audition_id'],
for_seating: $validData['for_seating'],
for_advancement: $validData['for_advancement'],
late_fee_waived: $validData['late_fee_waived'],
);
try {
$entry = $creator($validData['student_id'], $validData['audition_id'], $enter_for);
} catch (ManageEntryException $ex) {
return redirect()->route('admin.entries.index')->with('error', $ex->getMessage());
}
if ($validData['late_fee_waived']) {
$entry->addFlag('late_fee_waived');
}
@ -143,7 +123,7 @@ class EntryController extends Controller
return redirect(route('admin.entries.index'))->with('success', 'The entry has been added.');
}
public function edit(Entry $entry, CalculateScoreSheetTotal $calculator, ScoreService $scoreService)
public function edit(Entry $entry)
{
if ($entry->audition->hasFlag('seats_published')) {
return to_route('admin.entries.index')->with('error',
@ -157,31 +137,35 @@ class EntryController extends Controller
$students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get();
$auditions = Audition::orderBy('score_order')->get();
$scores = $entry->scoreSheets()->with('audition', 'judge')->get();
foreach ($scores as $score) {
$score->entry = $entry;
$score->valid = $scoreService->isScoreSheetValid($score);
$score->seating_total_score = $calculator('seating', $entry, $score->judge)[0];
$score->advancement_total_score = $calculator('advancement', $entry, $score->judge)[0];
}
// TODO: When updating Laravel, can we use the chaperone method I heard about ot load the entry back into the score
$scores = $entry->scoreSheets()->with('audition', 'judge', 'entry')->get();
return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores'));
$logEntries = AuditLogEntry::whereJsonContains('affected->entries', $entry->id)->orderBy('created_at', 'desc')->get();
return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores', 'logEntries'));
}
public function update(Request $request, Entry $entry, UpdateEntry $updater)
{
if ($entry->audition->hasFlag('seats_published')) {
// If the entry's current audition is published, we can't change it
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error',
'Entries in auditions with seats published cannot be modified');
'Entries in published auditions cannot be modified');
}
if ($entry->audition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error',
'Entries in auditions with advancement results published cannot be modified');
}
$validData = request()->validate([
'audition_id' => ['required', 'exists:auditions,id'],
'late_fee_waived' => ['sometimes'],
'for_seating' => ['sometimes'],
'for_advancement' => ['sometimes'],
]);
$proposedAudition = Audition::find($validData['audition_id']);
// If the entry's new audition is published, we can't change it
if ($proposedAudition->hasFlag('seats_published') || $proposedAudition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error',
'Entries cannot be moved to published auditions');
}
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
@ -191,11 +175,10 @@ class EntryController extends Controller
if (! auditionSetting('advanceTo')) {
$validData['for_seating'] = 1;
}
try {
$updater($entry, $validData);
} catch (ManageEntryException $e) {
return redirect()->route('admin.entries.index')->with('error', $e->getMessage());
}
/** @noinspection PhpUnhandledExceptionInspection */
$updater($entry, $validData);
if ($validData['late_fee_waived']) {
$entry->addFlag('late_fee_waived');
} else {
@ -205,17 +188,13 @@ class EntryController extends Controller
return to_route('admin.entries.index')->with('success', 'Entry updated successfully');
}
public function destroy(Request $request, Entry $entry)
public function destroy(Entry $entry)
{
if ($entry->audition->hasFlag('seats_published')) {
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error',
'Entries in auditions with seats published cannot be deleted');
'Entries in published auditions cannot be deleted');
}
if ($entry->audition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error',
'Entries in auditions with advancement results published cannot be deleted');
}
if (Seat::where('entry_id', $entry->id)->exists()) {
return redirect()->route('admin.entries.index')->with('error', 'Cannot delete an entry that is seated');
}
@ -224,21 +203,7 @@ class EntryController extends Controller
return redirect()->route('admin.entries.index')->with('error',
'Cannot delete an entry that has been scored');
}
if (auth()->user()) {
$message = 'Deleted entry '.$entry->id;
$affected = [
'entries' => [$entry->id],
'auditions' => [$entry->audition_id],
'schools' => [$entry->student->school_id],
'students' => [$entry->student_id],
];
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => $affected,
]);
}
$entry->delete();
return redirect()->route('admin.entries.index')->with('success', 'Entry Deleted');

View File

@ -5,9 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Event;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function abort;
use function compact;
class EventController extends Controller
@ -15,15 +13,16 @@ class EventController extends Controller
public function index()
{
$events = Event::all();
$renameModalXdata = '';
foreach ($events as $event) {
$renameModalXdata .= 'showRenameModal_'.$event->id.': false, ';
}
return view('admin.event.index', compact('events'));
return view('admin.event.index', compact('events', 'renameModalXdata'));
}
public function store(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'name' => ['required', 'unique:events,name'],
]);
@ -35,6 +34,21 @@ class EventController extends Controller
return redirect()->route('admin.events.index')->with('success', 'Event created successfully');
}
public function update(Request $request, Event $event)
{
if ($request->name !== $event->name) {
$validated = request()->validate([
'name' => ['required', 'unique:events,name'],
]);
$event->update([
'name' => $validated['name'],
]);
}
return redirect()->route('admin.events.index')->with('success', 'Event renamed successfully');
}
public function destroy(Request $request, Event $event)
{
if ($event->auditions()->count() > 0) {
@ -46,3 +60,4 @@ class EventController extends Controller
return redirect()->route('admin.events.index')->with('success', 'Event deleted successfully');
}
}
// TODO add form to modify an event

View File

@ -7,6 +7,8 @@ use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Response;
// TODO: Printing testing
/** @codeCoverageIgnore */
class ExportEntriesController extends Controller
{
public function __invoke()

View File

@ -7,6 +7,8 @@ use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Response;
// TODO: Printing testing
/** @codeCoverageIgnore */
class ExportResultsController extends Controller
{
public function __invoke()

View File

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\PrelimDefinitionStoreOrUpdateRequest;
use App\Models\Audition;
use App\Models\PrelimDefinition;
use App\Models\Room;
use App\Models\ScoringGuide;
use function view;
class PrelimDefinitionController extends Controller
{
public function index()
{
$prelims = PrelimDefinition::all();
return view('admin.prelim_definitions.index', compact('prelims'));
}
public function create()
{
$auditions = Audition::doesntHave('prelimDefinition')->get();
$rooms = Room::all();
$guides = ScoringGuide::all();
$method = 'POST';
$action = route('admin.prelim_definitions.store');
$prelim = false;
return view('admin.prelim_definitions.createOrUpdate', compact('auditions', 'rooms', 'guides', 'method', 'action', 'prelim'));
}
public function store(PrelimDefinitionStoreOrUpdateRequest $request)
{
$validated = $request->validated();
PrelimDefinition::create($validated);
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition created');
}
public function edit(PrelimDefinition $prelimDefinition)
{
$auditions = Audition::doesntHave('prelimDefinition')->get();
$rooms = Room::all();
$guides = ScoringGuide::all();
$method = 'PATCH';
$action = route('admin.prelim_definitions.update', $prelimDefinition);
$prelim = $prelimDefinition;
return view('admin.prelim_definitions.createOrUpdate', compact('auditions', 'rooms', 'guides', 'method', 'action', 'prelim'));
}
public function update(PrelimDefinition $prelimDefinition, PrelimDefinitionStoreOrUpdateRequest $request)
{
$validated = $request->validated();
$prelimDefinition->update($validated);
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition updated');
}
public function destroy(PrelimDefinition $prelimDefinition)
{
$prelimDefinition->delete();
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition deleted');
}
}

View File

@ -7,6 +7,8 @@ use App\Models\Entry;
use App\Models\Event;
use Illuminate\Support\Carbon;
// TODO: Printing testing
/** @codeCoverageIgnore */
class PrintCards extends Controller
{
public function index() // Display a form to select which cards to print
@ -25,11 +27,15 @@ class PrintCards extends Controller
public function print(\App\Actions\Print\PrintCards $printer)
{
//dump(request()->all());
if (request()->audition == null) {
return redirect()->back()->with('error', 'You must specify at least one audition');
// dump(request()->all());
// if (request()->audition == null) {
// return redirect()->back()->with('error', 'You must specify at least one audition');
// }
if (request()->audition) {
$selectedAuditionIds = array_keys(request()->audition);
} else {
$selectedAuditionIds = [];
}
$selectedAuditionIds = array_keys(request()->audition);
$cardQuery = Entry::whereIn('audition_id', $selectedAuditionIds);
// Process Filters
@ -60,6 +66,6 @@ class PrintCards extends Controller
}
$cards = $cards->sortBy($sorts);
$printer->print($cards);
//return view('admin.print_cards.print', compact('cards'));
// return view('admin.print_cards.print', compact('cards'));
}
}

View File

@ -8,6 +8,9 @@ use Codedge\Fpdf\Fpdf\Fpdf;
use function auditionSetting;
// TODO: Printing testing
/** @codeCoverageIgnore */
class PrintRoomAssignmentsController extends Controller
{
private $pdf;
@ -94,7 +97,7 @@ class PrintRoomAssignmentsController extends Controller
}
}
/** @codeCoverageIgnore */
class reportPDF extends FPDF
{
public function getPageBreakTrigger()

View File

@ -9,6 +9,8 @@ use App\Models\Room;
use function array_keys;
use function request;
// TODO: Printing testing
/** @codeCoverageIgnore */
class PrintSignInSheetsController extends Controller
{
public function index()

View File

@ -6,6 +6,8 @@ use App\Actions\Print\PrintStandNameTags;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\App;
// TODO: Printing testing
/** @codeCoverageIgnore */
class PrintStandNameTagsController extends Controller
{
public function __invoke()

View File

@ -7,6 +7,8 @@ use App\Http\Controllers\Controller;
use App\Models\Audition;
use Illuminate\Support\Facades\App;
// TODO: Rewrite Recap to work with new scoring code
/** @codeCoverageIgnore */
class RecapController extends Controller
{
public function selectAudition()

View File

@ -3,23 +3,41 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\BonusScoreDefinition;
use App\Models\Room;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use function auditionLog;
use function redirect;
class RoomController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get();
// Check if room id 0 exists, if not, create it and assign all unassigned auditions to it
if (! $rooms->contains('id', 0)) {
$unassignedRoom = Room::create([
'id' => 0,
'name' => 'Unassigned',
'description' => 'Auditions that have not been assigned to a room',
]);
$unassignedRoom->id = 0;
$unassignedRoom->save();
$auditionsToUpdate = Audition::whereNull('room_id')->get();
foreach ($auditionsToUpdate as $audition) {
$audition->room_id = 0;
$audition->save();
}
$rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get();
}
return view('admin.rooms.index', ['rooms' => $rooms]);
}
@ -30,14 +48,12 @@ class RoomController extends Controller
$rooms = Room::with(['judges.school', 'auditions'])->get();
$bonusScoresExist = BonusScoreDefinition::count() > 0;
return view('admin.rooms.judge_assignments', compact('usersWithoutRooms', 'usersWithRooms', 'rooms', 'bonusScoresExist'));
return view('admin.rooms.judge_assignments',
compact('usersWithoutRooms', 'usersWithRooms', 'rooms', 'bonusScoresExist'));
}
public function updateJudgeAssignment(Request $request, Room $room)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([
'judge' => 'exists:users,id',
]);
@ -51,29 +67,23 @@ class RoomController extends Controller
// detach judge on delete
$room->removeJudge($judge->id);
$message = 'Removed '.$judge->full_name().' from '.$room->name;
} else {
return redirect('/admin/rooms/judging_assignments')->with('error', 'Invalid request method.');
}
$affected['users'] = [$judge->id];
$affected['rooms'] = [$room->id];
auditionLog($message, $affected);
return redirect('/admin/rooms/judging_assignments')->with('success', $message);
return redirect(route('admin.rooms.judgingAssignment'))->with('success', $message);
}
public function create()
{
if (! Auth::user()->is_admin) {
abort(403);
}
return view('admin.rooms.create');
}
public function store(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([
'name' => 'required',
'name' => 'required|unique:rooms,name',
'description' => 'nullable',
]);
@ -87,11 +97,8 @@ class RoomController extends Controller
public function update(Request $request, Room $room)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([
'name' => 'required',
'name' => ['required', Rule::unique('rooms', 'name')->ignore($room->id)],
'description' => 'nullable',
]);
@ -104,12 +111,9 @@ class RoomController extends Controller
public function destroy(Room $room)
{
if (! Auth::user()->is_admin) {
abort(403);
}
if ($room->auditions()->count() > 0) {
return redirect()->route('admin.rooms.index')->with('error', 'Cannot delete room with auditions. First move the auditions to unassigned or another room');
return redirect()->route('admin.rooms.index')->with('error',
'Cannot delete room with auditions. First move the auditions to unassigned or another room');
}
$room->delete();

View File

@ -2,16 +2,16 @@
namespace App\Http\Controllers\Admin;
use App\Actions\Schools\CreateSchool;
use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller;
use App\Http\Requests\SchoolStoreRequest;
use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\SchoolEmailDomain;
use App\Models\User;
use App\Services\Invoice\InvoiceDataService;
use Illuminate\Support\Facades\Auth;
use function abort;
use function redirect;
use function request;
@ -38,46 +38,26 @@ class SchoolController extends Controller
public function show(School $school)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$logEntries = AuditLogEntry::whereJsonContains('affected->schools', $school->id)->orderBy('created_at', 'desc')->get();
return view('admin.schools.show', ['school' => $school]);
return view('admin.schools.show', compact('school', 'logEntries'));
}
public function edit(School $school)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$school->loadCount('students');
return view('admin.schools.edit', ['school' => $school]);
}
public function update(School $school)
public function update(SchoolStoreRequest $request, School $school)
{
request()->validate([
'name' => ['required'],
'address' => ['required'],
'city' => ['required'],
'state' => ['required'],
'zip' => ['required'],
]);
$school->update([
'name' => request('name'),
'address' => request('address'),
'city' => request('city'),
'state' => request('state'),
'zip' => request('zip'),
]);
$message = 'Modified school #'.$school->id.' - '.$school->name.' with address <br>'.$school->address.'<br>'.$school->city.', '.$school->state.' '.$school->zip;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['schools' => [$school->id]],
'name' => $request['name'],
'address' => $request['address'],
'city' => $request['city'],
'state' => $request['state'],
'zip' => $request['zip'],
]);
return redirect()->route('admin.schools.show', ['school' => $school->id])->with('success',
@ -86,54 +66,30 @@ class SchoolController extends Controller
public function create()
{
if (! Auth::user()->is_admin) {
abort(403);
}
return view('admin.schools.create');
}
public function store()
public function store(SchoolStoreRequest $request)
{
request()->validate([
'name' => ['required'],
'address' => ['required'],
'city' => ['required'],
'state' => ['required'],
'zip' => ['required'],
]);
$creator = app(CreateSchool::class);
$school = School::create([
'name' => request('name'),
'address' => request('address'),
'city' => request('city'),
'state' => request('state'),
'zip' => request('zip'),
]);
$message = 'Created school #'.$school->id.' - '.$school->name.' with address <br>'.$school->address.'<br>'.$school->city.', '.$school->state.' '.$school->zip;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['schools' => [$school->id]],
]);
$school = $creator(
$request['name'],
$request['address'],
$request['city'],
$request['state'],
$request['zip'],
);
return redirect('/admin/schools')->with('success', 'School '.$school->name.' created');
return redirect(route('admin.schools.index'))->with('success', 'School '.$school->name.' created');
}
public function destroy(School $school)
{
if ($school->students()->count() > 0) {
return to_route('admin.schools.index')->with('error', 'You cannot delete a school with students.');
return to_route('admin.schools.index')->with('error', 'You cannot delete a school that has students.');
}
$name = $school->name;
$message = 'Delete school #'.$school->id.' - '.$school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['schools' => [$school->id]],
]);
$school->delete();
return to_route('admin.schools.index')->with('success', 'School '.$school->name.' deleted');
@ -141,9 +97,6 @@ class SchoolController extends Controller
public function add_domain(School $school)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
// validate that the combination of school and domain is unique on the school_email_domains table
'domain' => ['required'],
@ -152,12 +105,6 @@ class SchoolController extends Controller
'school_id' => $school->id,
'domain' => request('domain'),
]);
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => 'Added '.request('domain').' as an email domain for school #'.$school->id.' - '.$school->name,
'affected' => ['schools' => [$school->id]],
]);
return redirect()->route('admin.schools.show', $school)->with('success', 'Domain Added');
@ -169,9 +116,11 @@ class SchoolController extends Controller
$domain->delete();
// return a redirect to the previous URL
return redirect()->back();
return redirect()->back()->with('success', 'Domain removed successfully.');
}
// TODO: Add testing for invoicing
/** @codeCoverageIgnore */
public function viewInvoice(School $school)
{
$invoiceData = $this->invoiceService->allData($school->id);
@ -184,8 +133,9 @@ class SchoolController extends Controller
if ($user->school_id !== $school->id) {
return redirect()->back()->with('error', 'That user is not at that school');
}
/** @noinspection PhpUnhandledExceptionInspection */
$headSetter->setHeadDirector($user);
return redirect()->back()->with('success', 'Head director set');
return redirect()->back()->with('success', 'Head director set successfully.');
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\School;
class SchoolEmailDomainController extends Controller
{
public function index()
{
$schools = School::with('emailDomains')->get();
return view('admin.schools.email_domains_index', compact('schools'));
}
}

View File

@ -3,13 +3,12 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\SubscoreDefinitionRequest;
use App\Models\ScoringGuide;
use App\Models\SubscoreDefinition;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use function abort;
use function auditionSetting;
use function request;
use function response;
@ -28,26 +27,19 @@ class ScoringGuideController extends Controller
public function store()
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'name' => ['required', 'unique:scoring_guides'],
]);
$guide = ScoringGuide::create([
ScoringGuide::create([
'name' => request('name'),
]);
return redirect(route('admin.scoring.index'))->with('success', 'Scoring guide created');
}
public function edit(Request $request, ScoringGuide $guide, string $tab = 'detail')
public function edit(ScoringGuide $guide, string $tab = 'detail')
{
if (! Auth::user()->is_admin) {
abort(403);
}
if ($tab == 'tiebreakOrder') {
$subscores = SubscoreDefinition::where('scoring_guide_id', $guide->id)->orderBy('tiebreak_order')->get();
} else {
@ -59,9 +51,6 @@ class ScoringGuideController extends Controller
public function update(ScoringGuide $guide)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'name' => ['required', 'unique:scoring_guides'],
]);
@ -75,12 +64,9 @@ class ScoringGuideController extends Controller
public function destroy(ScoringGuide $guide)
{
if (! Auth::user()->is_admin) {
abort(403);
}
if ($guide->auditions()->count() > 0) {
return redirect('/admin/scoring')->with('error', 'Cannot delete scoring guide with auditions');
return redirect('/admin/scoring')->with('error',
'Cannot delete scoring guide being used by one or more auditions');
}
$guide->delete();
@ -88,89 +74,64 @@ class ScoringGuideController extends Controller
return redirect('/admin/scoring')->with('success', 'Scoring guide deleted');
}
public function subscore_store(Request $request, ScoringGuide $guide)
public function subscore_store(SubscoreDefinitionRequest $request, ScoringGuide $guide)
{
if (! $guide->exists()) {
abort(409);
}
$validateData = request()->validate([
'name' => ['required'],
'max_score' => ['required', 'integer'],
'weight' => ['required', 'integer'],
'for_seating' => ['nullable', 'boolean'],
'for_advance' => ['nullable', 'boolean'],
]);
$for_seating = $request->has('for_seating') ? (bool) $request->input('for_seating') : false;
$for_advance = $request->has('for_advance') ? (bool) $request->input('for_advance') : false;
if (! auditionSetting('advanceTo')) {
$for_seating = true;
}
$validateData = $request->validated();
// Put the new subscore at the end of the list for both display and tiebreak order
$display_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('display_order') + 1;
$tiebreak_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('tiebreak_order') + 1;
$subscore = SubscoreDefinition::create([
if (! auditionSetting('advanceTo')) {
$validateData['for_advance'] = 0;
$validateData['for_seating'] = 1;
}
SubscoreDefinition::create([
'scoring_guide_id' => $guide->id,
'name' => $validateData['name'],
'max_score' => $validateData['max_score'],
'weight' => $validateData['weight'],
'display_order' => $display_order,
'tiebreak_order' => $tiebreak_order,
'for_seating' => $for_seating,
'for_advance' => $for_advance,
'for_seating' => $validateData['for_seating'],
'for_advance' => $validateData['for_advance'],
]);
return redirect(route('admin.scoring.edit', $guide))->with('success', 'Subscore added');
}
public function subscore_update(ScoringGuide $guide, SubscoreDefinition $subscore)
{
if (! Auth::user()->is_admin) {
abort(403);
}
if (! $guide->exists() || ! $subscore->exists()) {
abort(409);
}
public function subscore_update(
SubscoreDefinitionRequest $request,
ScoringGuide $guide,
SubscoreDefinition $subscore
) {
if ($subscore->scoring_guide_id !== $guide->id) { // Make sure the subscore were updating belongs to the guide
abort(409);
return redirect('/admin/scoring/guides/'.$subscore->scoring_guide_id.'/edit')->with('error',
'Cannot update a subscore for a different scoring guide');
}
$validateData = request()->validate([
'name' => ['required'],
'max_score' => ['required', 'integer'],
'weight' => ['required', 'integer'],
'for_seating' => ['nullable', 'boolean'],
'for_advance' => ['nullable', 'boolean'],
]);
$for_seating = request()->has('for_seating') ? (bool) request()->input('for_seating') : false;
$for_advance = request()->has('for_advance') ? (bool) request()->input('for_advance') : false;
$validateData = $validateData = $request->validated();
if (! auditionSetting('advanceTo')) {
$for_seating = true;
$validateData['for_advance'] = 0;
$validateData['for_seating'] = 1;
}
$subscore->update([
'name' => $validateData['name'],
'max_score' => $validateData['max_score'],
'weight' => $validateData['weight'],
'for_seating' => $for_seating,
'for_advance' => $for_advance,
'for_seating' => $validateData['for_seating'],
'for_advance' => $validateData['for_advance'],
]);
return redirect('/admin/scoring/guides/'.$guide->id.'/edit')->with('success', 'Subscore updated');
return redirect(route('admin.scoring.edit', $guide))->with('success', 'Subscore updated');
}
public function subscore_destroy(ScoringGuide $guide, SubscoreDefinition $subscore)
{
if (! Auth::user()->is_admin) {
abort(403);
}
if (! $guide->exists() || ! $subscore->exists()) {
abort(409);
}
if ($subscore->scoring_guide_id !== $guide->id) { // Make sure the subscore were updating belongs to the guide
abort(409);
return redirect(route('admin.scoring.edit', $subscore->scoring_guide_id))->with('error',
'Cannot delete a subscore for a different scoring guide');
}
$subscore->delete();
@ -181,9 +142,6 @@ class ScoringGuideController extends Controller
public function reorder_display(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order;
foreach ($order as $index => $id) {
$subscore = SubscoreDefinition::find($id);
@ -196,9 +154,6 @@ class ScoringGuideController extends Controller
public function reorder_tiebreak(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order;
foreach ($order as $index => $id) {
$subscore = SubscoreDefinition::find($id);

View File

@ -2,17 +2,16 @@
namespace App\Http\Controllers\Admin;
use App\Actions\Students\CreateStudent;
use App\Http\Controllers\Controller;
use App\Http\Requests\StudentStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\Event;
use App\Models\NominationEnsemble;
use App\Models\School;
use App\Models\Student;
use Illuminate\Support\Facades\Auth;
use function abort;
use function auth;
use function compact;
use function max;
@ -25,9 +24,6 @@ class StudentController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$filters = session('adminStudentFilters') ?? null;
$schools = School::orderBy('name')->get();
$students = Student::with(['school'])->withCount('entries')->orderBy('last_name')->orderBy('first_name');
@ -54,155 +50,52 @@ class StudentController extends Controller
public function create()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$minGrade = min(Audition::min('minimum_grade'), NominationEnsemble::min('minimum_grade'));
$maxGrade = max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade'));
$minGrade = $this->minimumGrade();
$maxGrade = $this->maximumGrade();
$schools = School::orderBy('name')->get();
return view('admin.students.create', ['schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]);
}
public function store()
public function store(StudentStoreRequest $request, CreateStudent $creator)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
'school_id' => ['required', 'exists:schools,id'],
/** @noinspection PhpUnhandledExceptionInspection */
$creator([
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'school_id' => $request['school_id'],
'optional_data' => $request->optional_data,
]);
if (Student::where('first_name', request('first_name'))
->where('last_name', request('last_name'))
->where('school_id', request('school_id'))
->exists()) {
return redirect('/admin/students/create')->with('error', 'This student already exists.');
}
$student = Student::create([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
'school_id' => request('school_id'),
]);
$message = 'Created student #'.$student->id.' - '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
]);
return redirect('/admin/students')->with('success', 'Created student successfully');
return redirect(route('admin.students.index'))->with('success', 'Student created successfully');
}
public function edit(Student $student)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$minGrade = min(Audition::min('minimum_grade'), NominationEnsemble::min('minimum_grade'));
$maxGrade = max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade'));
$minGrade = $this->minimumGrade();
$maxGrade = $this->maximumGrade();
$schools = School::orderBy('name')->get();
$student->loadCount('entries');
$entries = $student->entries;
$event_entries = $student->entries()->with('audition.flags')->get()->groupBy('audition.event_id');
$events = Event::all();
$event_entries = [];
foreach ($events as $event) {
$event_entries[$event->id] = $entries->filter(function ($entry) use ($event) {
return $event->id === $entry->audition->event_id;
});
// Check if doubler status can change
foreach ($event_entries[$event->id] as $entry) {
$entry->doubler_decision_frozen = $this->isDoublerStatusFrozen($entry, $event_entries[$event->id]);
}
}
$logEntries = AuditLogEntry::whereJsonContains('affected->students', $student->id)->orderBy('created_at',
'desc')->get();
return view('admin.students.edit',
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries'));
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries', 'logEntries'));
}
private function isDoublerStatusFrozen(Entry $entry, $entries)
public function update(StudentStoreRequest $request, Student $student)
{
// Can't change decision if results are published
if ($entry->audition->hasFlag('seats_published')) {
return true;
}
// Can't change decision if this is the only entry
if ($entries->count() === 1) {
return true;
}
// Can't change decision if this is the only entry with results not published
$unpublished = $entries->reject(function ($entry) {
return $entry->audition->hasFlag('seats_published');
});
if ($unpublished->count() < 2) {
return true;
}
// Can't change decision if we've accepted another audition
foreach ($entries as $checkEntry) {
if ($checkEntry->audition->hasFlag('seats_published') && ! $checkEntry->hasFlag('declined')) {
return true;
}
}
return false;
}
public function update(Student $student)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
'school_id' => ['required', 'exists:schools,id'],
]);
foreach ($student->entries as $entry) {
if ($entry->audition->minimum_grade > request('grade') || $entry->audition->maximum_grade < request('grade')) {
return redirect('/admin/students/'.$student->id.'/edit')->with('error',
'This student is entered in an audition that is not available to their new grade.');
}
}
if (Student::where('first_name', request('first_name'))
->where('last_name', request('last_name'))
->where('school_id', request('school_id'))
->where('id', '!=', $student->id)
->exists()) {
return redirect('/admin/students/'.$student->id.'/edit')->with('error',
'A student with that name already exists at that school');
}
$student->update([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
'school_id' => request('school_id'),
]);
$message = 'Updated student #'.$student->id.'<br>Name: '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'school_id' => $request['school_id'],
'optional_data' => $request->optional_data,
]);
return redirect('/admin/students')->with('success', 'Student updated');
@ -212,7 +105,7 @@ class StudentController extends Controller
public function destroy(Student $student)
{
if ($student->entries()->count() > 0) {
return to_route('admin.students.index')->with('error', 'You cannot delete a student with entries.');
return to_route('admin.students.index')->with('error', 'Student has entries and cannot be deleted');
}
$name = $student->full_name();
$message = 'Deleted student #'.$student->id.'<br>Name: '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
@ -230,8 +123,33 @@ class StudentController extends Controller
return to_route('admin.students.index')->with('success', 'Student '.$name.' deleted successfully.');
}
public function set_filter()
private function minimumGrade(): int
{
//
$nomMin = NominationEnsemble::min('minimum_grade');
$normMin = Audition::min('minimum_grade');
if (is_null($nomMin)) {
$minGrade = $normMin;
} else {
$minGrade = min($nomMin, $normMin);
}
return $minGrade;
}
private function maximumGrade(): int
{
$nomMax = NominationEnsemble::max('maximum_grade');
$normMax = Audition::max('maximum_grade');
if (is_null($nomMax)) {
$maxGrade = $normMax;
} else {
$maxGrade = max($nomMax, $normMax);
}
return $maxGrade;
}
}

View File

@ -1,7 +1,13 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Http\Controllers\Admin;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\UpdateUserPrivileges;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Actions\Schools\AssignUserToSchool;
use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller;
use App\Mail\NewUserPassword;
@ -9,7 +15,6 @@ use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
@ -20,9 +25,6 @@ class UserController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$users = User::with('school')->with('flags')->orderBy('last_name')->orderBy('first_name')->get();
return view('admin.users.index', ['users' => $users]);
@ -30,95 +32,65 @@ class UserController extends Controller
public function edit(User $user)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$schools = School::orderBy('name')->get();
return view('admin.users.edit', ['user' => $user, 'schools' => $schools]);
$schools = School::orderBy('name')->get();
$logEntries = AuditLogEntry::whereJsonContains('affected->users', $user->id)->orderBy('created_at',
'desc')->get();
$userActions = AuditLogEntry::where('user', $user->email)->orderBy('created_at', 'desc')->get();
return view('admin.users.edit', compact('user', 'schools', 'logEntries', 'userActions'));
}
public function create()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$schools = School::orderBy('name')->get();
return view('admin.users.create', ['schools' => $schools]);
}
public function update(Request $request, User $user, SetHeadDirector $headSetter)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$oldEmail = $user->email;
$wasAdmin = $user->is_admin;
$wasTab = $user->is_tab;
$validData = $request->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email'],
'cell_phone' => ['required'],
'judging_preference' => ['required'],
'school_id' => ['nullable', 'exists:schools,id'],
]);
$validData['is_admin'] = $request->get('is_admin') == 'on' ? 1 : 0;
$validData['is_tab'] = $request->get('is_tab') == 'on' ? 1 : 0;
$validData['is_head'] = $request->get('is_head') == 'on' ? 1 : 0;
$user->update([
'first_name' => $validData['first_name'],
'last_name' => $validData['last_name'],
'email' => $validData['email'],
'cell_phone' => $validData['cell_phone'],
'judging_preference' => $validData['judging_preference'],
'school_id' => $validData['school_id'],
'is_admin' => $validData['is_admin'],
'is_tab' => $validData['is_tab'],
]);
$user->refresh();
$logged_school = $user->school_id ? $user->school->name : 'No School';
$message = 'Updated user #'.$user->id.' - '.$oldEmail
.'<br>Name: '.$user->full_name()
.'<br>Email: '.$user->email
.'<br>Cell Phone: '.$user->cell_phone
.'<br>Judging Pref: '.$user->judging_preference
.'<br>School: '.$logged_school;
public function update(
Request $request,
User $user,
SetHeadDirector $headSetter,
UpdateUserProfileInformation $profileUpdater,
AssignUserToSchool $schoolAssigner,
UpdateUserPrivileges $privilegesUpdater
) {
// Update basic profile data
$profileData = [
'first_name' => $request->get('first_name'),
'last_name' => $request->get('last_name'),
'email' => $request->get('email'),
'cell_phone' => $request->get('cell_phone'),
'judging_preference' => $request->get('judging_preference'),
];
$profileUpdater->update($user, $profileData);
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
if ($user->is_admin != $wasAdmin) {
$messageStart = $user->is_admin ? 'Granted admin privileges to ' : 'Revoked admin privileges from ';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $messageStart.$user->full_name().' - '.$user->email,
'affected' => ['users' => [$user->id]],
]);
// Deal with school assignment
dump($request->get('school_id'));
if ($user->school_id != $request->get('school_id')) {
$schoolAssigner($user, $request->get('school_id'));
}
if ($user->is_tab != $wasTab) {
$messageStart = $user->is_tab ? 'Granted tabulation privileges to ' : 'Revoked tabulation privileges from ';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $messageStart.$user->full_name().' - '.$user->email,
'affected' => ['users' => [$user->id]],
]);
// Deal with the head director flag
if ($request->has('head_director')) {
$headSetter($user);
} else {
$user->removeFlag('head_director');
}
if ($user->hasFlag('head_director') != $validData['is_head'] && ! is_null($user->school_id)) {
if ($validData['is_head']) {
$headSetter->setHeadDirector($user);
} else {
$user->removeFlag('head_director');
$logMessage = 'Removed '.$user->full_name().' as head director at '.$user->school->name;
$logAffected = ['users' => [$user->id], 'schools' => [$user->school_id]];
auditionLog($logMessage, $logAffected);
}
// Deal with privileges
if ($request->has('is_admin')) {
$privilegesUpdater($user, 'grant', 'admin');
} else {
$privilegesUpdater($user, 'revoke', 'admin');
}
if ($request->has('is_tab')) {
$privilegesUpdater($user, 'grant', 'tab');
} else {
$privilegesUpdater($user, 'revoke', 'tab');
}
return redirect('/admin/users');
@ -126,60 +98,23 @@ class UserController extends Controller
public function store(Request $request)
{
$request->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email', 'unique:users'],
]);
// Generate a random password
$userCreator = app(CreateNewUser::class);
$randomPassword = Str::random(12);
$user = User::make([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'email' => request('email'),
'cell_phone' => request('cell_phone'),
'judging_preference' => request('judging_preference'),
'password' => Hash::make($randomPassword),
$data = request()->all();
$data['password'] = $randomPassword;
$data['password_confirmation'] = $randomPassword;
$newDirector = $userCreator->create($data);
$newDirector->update([
'school_id' => $request->get('school_id') ?? null,
]);
if (! is_null(request('school_id'))) {
$request->validate([
'school_id' => ['exists:schools,id'],
]);
}
$user->school_id = request('school_id');
$user->save();
$message = 'Created user '.$user->email.' - '.$user->full_name().'<br>Cell Phone: '.$user->cell_phone.'<br>Judging Pref: '.$user->judging_preference;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
if ($user->school_id) {
$message = 'Set user '.$user->full_name().' ('.$user->email.') as a director at '.$user->school->name.'(#'.$user->school->id.')';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'users' => [$user->id],
'schools' => [$user->id],
],
]);
}
Mail::to($user->email)->send(new NewUserPassword($user, $randomPassword));
Mail::to($newDirector->email)->send(new NewUserPassword($newDirector, $randomPassword));
return redirect('/admin/users');
return redirect(route('admin.users.index'))->with('success', 'Director added');
}
public function destroy(User $user)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$message = 'Deleted user '.$user->email;
AuditLogEntry::create([
'user' => auth()->user()->email,
@ -191,4 +126,22 @@ class UserController extends Controller
return redirect()->route('admin.users.index')->with('success', 'User deleted successfully');
}
public function setPassword(User $user, Request $request)
{
$validated = $request->validate([
'admin_password' => ['required', 'string', 'current_password:web'],
'new_password' => ['required', 'string', 'confirmed', 'min:8'],
]);
$user->forceFill([
'password' => Hash::make($validated['new_password']),
])->save();
auditionLog('Manually set password for '.$user->email, [
'users' => [$user->id],
]);
return redirect()->route('admin.users.index')->with('success',
'Password changed successfully for '.$user->email);
}
}

View File

@ -16,7 +16,7 @@ class YearEndResetController extends Controller
public function execute()
{
$cleanUpProcedure = new YearEndCleanup;
$cleanUpProcedure = app(YearEndCleanup::class);
$options = request()->options;
$cleanUpProcedure($options);
auditionLog('Executed year end reset.', []);

View File

@ -2,10 +2,6 @@
namespace App\Http\Controllers;
use App\Actions\Entries\GetEntrySeatingResult;
use App\Actions\Tabulation\CalculateEntryScore;
use App\Actions\Tabulation\RankAuditionEntries;
use App\Models\AuditionFlag;
use App\Models\School;
use App\Services\Invoice\InvoiceDataService;
use Illuminate\Support\Facades\Auth;
@ -28,40 +24,15 @@ class DashboardController extends Controller
}
public function dashboard(
CalculateEntryScore $scoreCalc,
GetEntrySeatingResult $resultGenerator,
RankAuditionEntries $ranker
) {
// Info for director results report
$entries = Auth::user()->entries;
$entries = $entries->filter(function ($entry) {
return $entry->audition->hasFlag('seats_published');
});
$entries = $entries->sortBy(function ($entry) {
return $entry->student->full_name(true);
});
$scores = [];
$results = [];
$ranks = [];
foreach ($entries as $entry) {
$results[$entry->id] = $resultGenerator->getResult($entry);
if (! $entry->hasFlag('no_show') && ! $entry->hasFlag('failed_prelim')) {
$scores[$entry->id] = $scoreCalc->calculate('seating', $entry);
$auditionResults = $ranker->rank('seating', $entry->audition);
$ranks[$entry->id] = $auditionResults->firstWhere('id', $entry->id)->raw_rank;
}
}
$showRecapLink = AuditionFlag::where('flag_name', 'seats_published')->count() > 0;
return view('dashboard.dashboard', compact('entries', 'scores', 'results', 'ranks', 'showRecapLink'));
return view('dashboard.dashboard');
// return view('dashboard.dashboard');
}
public function my_school()
{
if (Auth::user()->school) {
return redirect('/schools/'.Auth::user()->school->id);
return redirect(route('schools.show', auth()->user()->school));
}
$possibilities = Auth::user()->possibleSchools();
if (count($possibilities) < 1) {

View File

@ -2,88 +2,67 @@
namespace App\Http\Controllers;
use App\Models\AuditLogEntry;
use App\Http\Requests\DoublerRequestsStoreRequest;
use App\Models\DoublerRequest;
use App\Models\Event;
use App\Models\Student;
use App\Services\DoublerService;
use Barryvdh\Debugbar\Facades\Debugbar;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use function auth;
use function compact;
use function request;
use function to_route;
class DoublerRequestController extends Controller
{
public function index(DoublerService $doublerService)
/**
* Display a listing of the resource.
*
* Data sent to view:
* - events - all existing events
* - existingRequests - previously made requests for each event, keyed by student id
* existingRequest[eventId][student id]-> Request
* - doublers - existing doublers, grouped by event. Keyed by event_id and student_id
*
* @return Application|Factory|View|\Illuminate\Foundation\Application|\Illuminate\View\View
*/
public function index()
{
$events = Event::all();
$students = auth()->user()->school->students;
$studentIds = $students->pluck('id');
$existingRequests = DoublerRequest::whereIn('student_id', $studentIds)->get();
$doublers = [];
foreach ($events as $event) {
$event_doublers = $doublerService->doublersForEvent($event);
$doublers[$event->id] = $event_doublers;
}
$existingRequests = auth()->user()->school->doublerRequests
->groupBy('event_id')
->map(function ($requestsForEvent) {
return $requestsForEvent->keyBy('student_id');
});
$doublers = auth()->user()->school->doublers()
->with('student')
->with('event')
->get()
->groupBy('event_id');
return view('doubler_request.index', compact('events', 'doublers', 'students', 'existingRequests'));
return view('doubler_request.index', compact('events', 'doublers', 'existingRequests'));
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function makeRequest()
public function makeRequest(DoublerRequestsStoreRequest $request)
{
foreach (request()->get('doubler_requests') as $event_id => $requests) {
if (! Event::find($event_id)->exists()) {
return to_route('doubler_request.index')->with('error', 'Invalid event id specified');
}
$thisEvent = Event::find($event_id);
foreach ($requests as $student_id => $request) {
if (! Student::find($student_id)->exists()) {
return to_route('doubler_request.index')->with('error', 'Invalid student id specified');
}
$thisStudent = Student::find($student_id);
if (! $request) {
$oldRequest = DoublerRequest::where('student_id', $student_id)
->where('event_id', $event_id)
->first();
if ($oldRequest) {
Debugbar::info('hit');
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => 'Removed doubler request for '.$thisStudent->full_name().' in '.$thisEvent->name,
'affected' => ['students' => [$student_id]],
]);
$oldRequest->delete();
}
foreach ($request->getDoublerRequests() as $thisRequest) {
if (! $thisRequest['request']) {
DoublerRequest::where('event_id', $thisRequest['event_id'])
->where('student_id', $thisRequest['student_id'])->delete();
continue;
}
DoublerRequest::upsert([
'event_id' => $event_id,
'student_id' => $student_id,
'request' => $request,
],
uniqueBy: ['event_id', 'student_id'],
update: ['request']
);
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => 'Made doubler request for '.$thisStudent->full_name().' in '.$thisEvent->name.'<br>Request: '.$request,
'affected' => ['students' => [$student_id]],
]);
continue;
}
DoublerRequest::upsert([
'event_id' => $thisRequest['event_id'],
'student_id' => $thisRequest['student_id'],
'request' => $thisRequest['request'],
],
uniqueBy: ['event_id', 'student_id'],
update: ['request']
);
}
echo 'hi';
return to_route('doubler_request.index')->with('success', 'Recorded doubler requests');
}
}

View File

@ -3,11 +3,9 @@
namespace App\Http\Controllers;
use App\Actions\Entries\CreateEntry;
use App\Exceptions\ManageEntryException;
use App\Http\Requests\EntryStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -17,11 +15,19 @@ class EntryController extends Controller
{
public function index()
{
if (! auth()->user()->school_id) {
abort(403);
}
$entries = Auth::user()->entries()->with(['student', 'audition'])->get();
$entries = $entries->sortBy(function ($entry) {
return $entry->student->last_name.$entry->student->first_name.$entry->audition->score_order;
});
$entries = Auth::user()->entries()
->select('entries.*')
->join('students as s', 's.id', '=', 'entries.student_id')
->join('auditions as a', 'a.id', '=', 'entries.audition_id')
->with(['student', 'audition'])
->orderBy('s.last_name')
->orderBy('s.first_name')
->orderBy('a.score_order')
->get();
$auditions = Audition::open()->get();
$students = Auth::user()->students;
$students->load('school');
@ -29,37 +35,15 @@ class EntryController extends Controller
return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]);
}
public function store(Request $request, CreateEntry $creator)
public function store(EntryStoreRequest $request, CreateEntry $creator)
{
if ($request->user()->cannot('create', Entry::class)) {
abort(403);
}
$validData = $request->validate([
'student_id' => ['required', 'exists:students,id'],
'audition_id' => ['required', 'exists:auditions,id'],
]);
$audition = Audition::find($validData['audition_id']);
$currentDate = Carbon::now('America/Chicago');
$currentDate = $currentDate->format('Y-m-d');
if ($audition->entry_deadline < $currentDate) {
return redirect()->route('entries.index')->with('error', 'The entry deadline for that audition has passed');
}
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
$enter_for = [];
if ($validData['for_seating']) {
$enter_for[] = 'seating';
}
if ($validData['for_advancement']) {
$enter_for[] = 'advancement';
}
try {
$creator($validData['student_id'], $validData['audition_id'], $enter_for);
} catch (ManageEntryException $ex) {
return redirect()->route('entries.index')->with('error', $ex->getMessage());
}
$validData = $request->validatedWithEnterFor();
$creator(
$validData['student_id'],
$validData['audition_id'],
for_seating: $validData['for_seating'],
for_advancement: $validData['for_advancement'],
);
return redirect()->route('entries.index')->with('success', 'The entry has been added.');
}
@ -69,21 +53,7 @@ class EntryController extends Controller
if ($request->user()->cannot('delete', $entry)) {
abort(403);
}
if (auth()->user()) {
$message = 'Deleted entry '.$entry->id;
$affected = [
'entries' => [$entry->id],
'auditions' => [$entry->audition_id],
'schools' => [$entry->student->school_id],
'students' => [$entry->student_id],
];
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => $affected,
]);
}
$entry->delete();
return redirect()->route('entries.index')->with('success',

View File

@ -20,14 +20,14 @@ class FilterController extends Controller
session(['adminEntryFilters' => $filters]);
return redirect('/admin/entries')->with('success', 'Filters Applied');
return redirect(route('admin.entries.index'))->with('success', 'Filters Applied');
}
public function clearAdminEntryFilter(Request $request)
{
session()->forget('adminEntryFilters');
return redirect('/admin/entries')->with('success', 'Filters Cleared');
return redirect(route('admin.entries.index'))->with('success', 'Filters Cleared');
}
public function adminStudentFilter(Request $request)
@ -40,7 +40,7 @@ class FilterController extends Controller
session(['adminStudentFilters' => $filters]);
return redirect()->back()->with('success', 'Filters Applied');
return redirect(route('admin.students.index'))->with('success', 'Filters Applied');
}
public function clearAdminStudentFilter()

View File

@ -12,16 +12,23 @@ use function redirect;
class BonusScoreEntryController extends Controller
{
/**
* Displays a form for a judge to enter a bonus score for an entry.
*/
public function __invoke(Entry $entry)
{
// We can't submit another bonus score for this entry if we have already submitted one.
if (BonusScore::where('entry_id', $entry->id)->where('user_id', Auth::user()->id)->exists()) {
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('error', 'You have already judged that entry');
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('error',
'You have already judged that entry');
}
/** @var BonusScoreDefinition $bonusScore */
$bonusScore = $entry->audition->bonusScore()->first();
if (! $bonusScore->judges->contains(auth()->id())) {
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this entry');
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge that entry');
}
$maxScore = $bonusScore->max_score;
$bonusName = $bonusScore->name;

View File

@ -10,12 +10,15 @@ use Illuminate\Support\Facades\Auth;
class BonusScoreEntryListController extends Controller
{
/**
* Lists entries for a bonus score so the judge may select one to score.
*/
public function __invoke(Audition $audition)
{
/** @var BonusScoreDefinition $bonusScore */
$bonusScore = $audition->bonusScore()->first();
if (! $bonusScore->judges->contains(auth()->id())) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge this bonus score');
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that bonus score');
}
$entries = $audition->entries()->orderBy('draw_number')->get();
$entries = $entries->reject(fn ($entry) => $entry->hasFlag('no_show'));

View File

@ -3,7 +3,7 @@
namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterBonusScore;
use App\Exceptions\ScoreEntryException;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Entry;
use Illuminate\Support\Facades\App;
@ -14,15 +14,17 @@ class BonusScoreRecordController extends Controller
public function __invoke(Entry $entry)
{
$enterBonusScore = App::make(EnterBonusScore::class);
$max = $entry->audition->bonusScore()->first()->max_score;
$validData = request()->validate([
'score' => 'required|integer',
'score' => 'required|integer|min:0|max:'.$max,
]);
try {
$enterBonusScore(Auth::user(), $entry, $validData['score']);
} catch (ScoreEntryException $ex) {
return redirect()->back()->with('error', 'Score Entry Error - '.$ex->getMessage());
} catch (AuditionAdminException $ex) {
return redirect(route('dashboard'))->with('error', 'Score Entry Error - '.$ex->getMessage());
}
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('Score Recorded Successfully');
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('success',
'Score Recorded Successfully');
}
}

View File

@ -3,22 +3,19 @@
namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionServiceException;
use App\Exceptions\ScoreEntryException;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\JudgeAdvancementVote;
use App\Models\ScoreSheet;
use App\Services\AuditionService;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use function compact;
use function redirect;
use function url;
class JudgingController extends Controller
{
@ -31,19 +28,25 @@ class JudgingController extends Controller
public function index()
{
$rooms = Auth::user()->judgingAssignments()->with('auditions')->get();
$rooms = Auth::user()->judgingAssignments()->with('auditions')->with('prelimAuditions')->get();
$bonusScoresToJudge = Auth::user()->bonusJudgingAssignments()->with('auditions')->get();
//$rooms->load('auditions');
// $rooms->load('auditions');
return view('judging.index', compact('rooms', 'bonusScoresToJudge'));
}
public function auditionEntryList(Request $request, Audition $audition)
{
// TODO: Add error message if scoring guide is not set
if ($request->user()->cannot('judge', $audition)) {
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this audition');
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge that audition');
}
$entries = Entry::where('audition_id', '=', $audition->id)->orderBy('draw_number')->with('audition')->get();
// If there is a prelim audition, only show entries that have passed the prelim
if ($audition->prelimDefinition) {
$entries = $entries->reject(fn ($entry) => ! $entry->hasFlag('passed_prelim'));
}
$subscores = $audition->scoringGuide->subscores()->orderBy('display_order')->get();
$votes = JudgeAdvancementVote::where('user_id', Auth::id())->get();
@ -68,6 +71,13 @@ class JudgingController extends Controller
return redirect()->route('judging.auditionEntryList', $entry->audition)->with('error',
'The requested entry is marked as a no-show. Scores cannot be entered.');
}
// Turn away users if the entry is flagged as a failed-prelim
if ($entry->hasFlag('failed_prelim')) {
return redirect()->route('judging.auditionEntryList', $entry->audition)->with('error',
'The requested entry is marked as having failed a prelim. Scores cannot be entered.');
}
$oldSheet = ScoreSheet::where('user_id', Auth::id())->where('entry_id', $entry->id)->value('subscores') ?? null;
$oldVote = JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->first();
$oldVote = $oldVote ? $oldVote->vote : 'noVote';
@ -78,15 +88,11 @@ class JudgingController extends Controller
public function saveScoreSheet(Request $request, Entry $entry, EnterScore $enterScore)
{
if ($request->user()->cannot('judge', $entry->audition)) {
abort(403, 'You are not assigned to judge this entry');
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this entry');
}
// Validate form data
try {
$subscores = $this->auditionService->getSubscores($entry->audition, 'all');
} catch (AuditionServiceException $e) {
return redirect()->back()->with('error', 'Unable to get subscores - '.$e->getMessage());
}
$subscores = $entry->audition->subscoreDefinitions;
$validationChecks = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
@ -94,16 +100,17 @@ class JudgingController extends Controller
$validatedData = $request->validate($validationChecks);
// Enter the score
/** @noinspection PhpUnhandledExceptionInspection */
try {
$enterScore(Auth::user(), $entry, $validatedData['score']);
} catch (ScoreEntryException $e) {
return redirect()->back()->with('error', 'Error saving score - '.$e->getMessage());
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
}
// Deal with an advancement vote if needed
$this->advancementVote($request, $entry);
return redirect('/judging/audition/'.$entry->audition_id)->with('success',
return redirect(route('judging.auditionEntryList', $entry->audition))->with('success',
'Entered scores for '.$entry->audition->name.' '.$entry->draw_number);
}
@ -111,8 +118,10 @@ class JudgingController extends Controller
public function updateScoreSheet(Request $request, Entry $entry, EnterScore $enterScore)
{
if ($request->user()->cannot('judge', $entry->audition)) {
abort(403, 'You are not assigned to judge this entry');
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this entry');
}
// We can't update a scoresheet that doesn't exist
$scoreSheet = ScoreSheet::where('user_id', Auth::id())->where('entry_id', $entry->id)->first();
if (! $scoreSheet) {
return redirect()->back()->with('error', 'Attempt to edit non existent score sheet');
@ -120,11 +129,8 @@ class JudgingController extends Controller
Gate::authorize('update', $scoreSheet);
// Validate form data
try {
$subscores = $this->auditionService->getSubscores($entry->audition, 'all');
} catch (AuditionServiceException $e) {
return redirect()->back()->with('error', 'Error getting subscores - '.$e->getMessage());
}
$subscores = $entry->audition->subscoreDefinitions;
$validationChecks = [];
foreach ($subscores as $subscore) {
@ -133,38 +139,29 @@ class JudgingController extends Controller
$validatedData = $request->validate($validationChecks);
// Enter the score
try {
$enterScore(Auth::user(), $entry, $validatedData['score'], $scoreSheet);
} catch (ScoreEntryException $e) {
return redirect()->back()->with('error', 'Error updating score - '.$e->getMessage());
}
$enterScore(Auth::user(), $entry, $validatedData['score'], $scoreSheet);
$this->advancementVote($request, $entry);
return redirect('/judging/audition/'.$entry->audition_id)->with('success',
return redirect(route('judging.auditionEntryList', $entry->audition))->with('success',
'Updated scores for '.$entry->audition->name.' '.$entry->draw_number);
}
protected function advancementVote(Request $request, Entry $entry)
{
if ($request->user()->cannot('judge', $entry->audition)) {
abort(403, 'You are not assigned to judge this entry');
}
if ($entry->for_advancement and auditionSetting('advanceTo')) {
$request->validate([
'advancement-vote' => ['required', 'in:yes,no,dq'],
]);
try {
JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->delete();
JudgeAdvancementVote::create([
'user_id' => Auth::user()->id,
'entry_id' => $entry->id,
'vote' => $request->input('advancement-vote'),
]);
} catch (Exception) {
return redirect(url()->previous())->with('error', 'Error saving advancement vote');
}
JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->delete();
JudgeAdvancementVote::create([
'user_id' => Auth::user()->id,
'entry_id' => $entry->id,
'vote' => $request->input('advancement-vote'),
]);
}
return null;

View File

@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterPrelimScore;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class PrelimJudgingController extends Controller
{
public function prelimEntryList(PrelimDefinition $prelimDefinition)
{
if (auth()->user()->cannot('judge', $prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
$entries = $prelimDefinition->audition->entries;
$subscores = $prelimDefinition->scoringGuide->subscores()->orderBy('display_order')->get();
$published = $prelimDefinition->audition->hasFlag('seats_published');
$prelimScoresheets = PrelimScoreSheet::where('user_id', Auth::id())->get()->keyBy('entry_id');
return view('judging.prelim_entry_list',
compact('prelimDefinition', 'entries', 'subscores', 'published', 'prelimScoresheets'));
}
public function prelimScoreEntryForm(Entry $entry)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
if ($entry->audition->hasFlag('seats_published')) {
return redirect()->route('dashboard')->with('error',
'Scores for entries in published auditions cannot be modified.');
}
if ($entry->hasFlag('no_show')) {
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('error',
'The requested entry is marked as a no-show. Scores cannot be entered.');
}
$oldSheet = PrelimScoreSheet::where('user_id', Auth::id())->where('entry_id',
$entry->id)->value('subscores') ?? null;
if ($oldSheet) {
$formRoute = 'update.savePrelimScoreSheet';
$formMethod = 'PATCH';
} else {
$formRoute = 'judging.savePrelimScoreSheet';
$formMethod = 'POST';
}
return view('judging.prelim_entry_form', compact('entry', 'oldSheet', 'formRoute', 'formMethod'));
}
/**
* @throws AuditionAdminException
*/
public function savePrelimScoreSheet(Entry $entry, Request $request, EnterPrelimScore $scribe)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
// Validate form data
$subscores = $entry->audition->prelimDefinition->scoringGuide->subscores;
$validationChecks = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
}
$validatedData = $request->validate($validationChecks);
// Enter the score
$scribe(auth()->user(), $entry, $validatedData['score']);
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('success',
'Entered prelim scores for '.$entry->audition->name.' '.$entry->draw_number);
}
public function updatePrelimScoreSheet(Entry $entry, Request $request, EnterPrelimScore $scribe)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
// Validate form data
$subscores = $entry->audition->prelimDefinition->scoringGuide->subscores;
$validationChecks = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
}
$validatedData = $request->validate($validationChecks);
// Get the existing score
$scoreSheet = PrelimScoreSheet::where('user_id', auth()->user()->id)->where('entry_id', $entry->id)->first();
if (! $scoreSheet) {
return redirect()->back()->with('error', 'No score sheet exists.');
}
// Update the score
$scribe(auth()->user(), $entry, $validatedData['score'], $scoreSheet);
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('success',
'Updated prelim scores for '.$entry->audition->name.' '.$entry->draw_number);
}
}

View File

@ -2,97 +2,56 @@
namespace App\Http\Controllers;
use App\Models\Audition;
use App\Models\Entry;
use function compact;
class MonitorController extends Controller
{
public function index()
{
if (! auth()->user()->hasFlag('monitor')) {
return redirect()->route('dashboard')->with('error', 'You are not assigned as a monitor');
abort(403);
}
$method = 'GET';
$formRoute = 'monitor.enterFlag';
$title = 'Flag Entry';
return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
$auditions = Audition::orderBy('score_order')->with('flags')->get();
$audition = null;
return view('monitor.index', compact('audition', 'auditions'));
}
public function flagForm()
public function auditionStatus(Audition $audition)
{
if (! auth()->user()->hasFlag('monitor')) {
return redirect()->route('dashboard')->with('error', 'You are not assigned as a monitor');
}
$validData = request()->validate([
'entry_id' => ['required', 'integer', 'exists:entries,id'],
]);
$entry = Entry::find($validData['entry_id']);
// If the entries audition is published, bounce out
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advance_published')) {
return redirect()->route('monitor.index')->with('error', 'Cannot set flags while results are published');
abort(403);
}
// If entry has scores, bounce on out
if ($entry->scoreSheets()->count() > 0) {
return redirect()->route('monitor.index')->with('error', 'That entry has existing scores');
if ($audition->hasFlag('seats_published') || $audition->hasFlag('advancement_published')) {
return redirect()->route('monitor.index')->with('error', 'Results for that audition are published');
}
return view('monitor_entry_flag_form', compact('entry'));
$auditions = Audition::orderBy('score_order')->with('flags')->get();
$entries = $audition->entries()->with('flags')->with('student.school')->withCount([
'prelimScoreSheets', 'scoreSheets',
])->orderBy('draw_number')->get();
return view('monitor.index', compact('audition', 'auditions', 'entries'));
}
public function storeFlag(Entry $entry)
public function toggleNoShow(Entry $entry)
{
if (! auth()->user()->hasFlag('monitor')) {
return redirect()->route('dashboard')->with('error', 'You are not assigned as a monitor');
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
return redirect()->route('monitor.index')->with('error', 'Results for that audition are published');
}
// If the entries audition is published, bounce out
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advance_published')) {
return redirect()->route('monitor.index')->with('error', 'Cannot set flags while results are published');
}
// If entry has scores, bounce on out
if ($entry->scoreSheets()->count() > 0) {
return redirect()->route('monitor.index')->with('error', 'That entry has existing scores');
}
$action = request()->input('action');
$result = match ($action) {
'failed-prelim' => $this->setFlag($entry, 'failed_prelim'),
'no-show' => $this->setFlag($entry, 'no_show'),
'clear' => $this->setFlag($entry, 'clear'),
default => redirect()->route('monitor.index')->with('error', 'Invalid action requested'),
};
if (! $result) {
return redirect()->route('monitor.index')->with('error', 'Failed to set flag');
}
return redirect()->route('monitor.index')->with('success', 'Flag set for entry #'.$entry->id);
}
private function setFlag(Entry $entry, string $flag)
{
if ($flag === 'no_show') {
$entry->removeFlag('failed_prelim');
$entry->addFlag('no_show');
return true;
}
if ($flag === 'failed_prelim') {
$entry->addFlag('failed_prelim');
$entry->addFlag('no_show');
return true;
}
if ($flag === 'clear') {
$entry->removeFlag('failed_prelim');
if ($entry->hasFlag('no_show')) {
$entry->removeFlag('no_show');
return true;
return redirect()->back()->with('success', 'No Show Flag Cleared');
}
$entry->addFlag('no_show');
return false;
return redirect()->back()->with('success', 'No Show Entered');
}
}

View File

@ -9,6 +9,10 @@ use Illuminate\Http\Request;
use function auditionSetting;
/**
* @codeCoverageIgnore
* TODO: Figure out testing for printing
*/
class PdfInvoiceController extends Controller
{
protected $pdf;

View File

@ -6,6 +6,8 @@ use App\Actions\Tabulation\RankAuditionEntries;
use App\Models\Audition;
use Illuminate\Support\Facades\App;
/** @codeCoverageIgnore */
// TODO: Rewrite Recap
class RecapController extends Controller
{
public function selectAudition()

View File

@ -8,6 +8,7 @@ use App\Models\Ensemble;
use App\Models\Entry;
use App\Models\Seat;
use App\Services\AuditionService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\View;
@ -28,9 +29,11 @@ class ResultsPage extends Controller
*/
public function __invoke(Request $request)
{
Model::preventLazyLoading(false);
$cacheKey = 'publicResultsPage';
if (Cache::has($cacheKey)) {
/** @codeCoverageIgnore */
return response(Cache::get($cacheKey));
}
@ -91,9 +94,4 @@ class ResultsPage extends Controller
return response($content);
}
private function generateResultsPage()
{
}
}

View File

@ -2,93 +2,48 @@
namespace App\Http\Controllers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Schools\AddSchoolEmailDomain;
use App\Actions\Schools\AssignUserToSchool;
use App\Actions\Schools\CreateSchool;
use App\Actions\Schools\SetHeadDirector;
use App\Exceptions\AuditionAdminException;
use App\Http\Requests\SchoolStoreRequest;
use App\Mail\NewUserPassword;
use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\SchoolEmailDomain;
use App\Models\User;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use function abort;
use function auditionLog;
use function redirect;
use function request;
class SchoolController extends Controller
{
public function store(Request $request, SetHeadDirector $headSetter): RedirectResponse
public function store(SchoolStoreRequest $request, SetHeadDirector $headSetter): RedirectResponse
{
if ($request->user()->cannot('create', School::class)) {
abort(403);
}
request()->validate([
'name' => ['required', 'min:3', 'max:30'],
'address' => ['required'],
'city' => ['required'],
'state' => ['required', 'min:2', 'max:2'],
'zip' => ['required', 'min:5', 'max:10'],
]);
$creator = app(CreateSchool::class);
$school = School::create([
'name' => request('name'),
'address' => request('address'),
'city' => request('city'),
'state' => request('state'),
'zip' => request('zip'),
]);
$message = 'Created school #'.$school->id.' - '.$school->name.' with address <br>'.$school->address.'<br>'.$school->city.', '.$school->state.' '.$school->zip;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['schools' => [$school->id]],
]);
$school = $creator(
$request['name'],
$request['address'],
$request['city'],
$request['state'],
$request['zip'],
);
if (! Auth::user()->school) {
Auth::user()->update([
'school_id' => $school->id,
]);
$message = 'Set user '.auth()->user()->full_name().' ('.auth()->user()->email.') as a director at '.$school->name.'(#'.$school->id.')';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'users' => [auth()->user()->id],
'schools' => [$school->id],
],
]);
SchoolEmailDomain::create([
'school_id' => $school->id,
'domain' => Auth::user()->emailDomain(),
]);
$message = 'Added '.auth()->user()->emailDomain().' as an email domain for '.$school->name.' (#'.$school->id.')';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'schools' => [$school->id],
],
]);
auth()->user()->refresh();
try {
$headSetter->setHeadDirector(auth()->user());
} catch (AuditionAdminException $e) {
redirect(route('schools.show', $school))->with('error', 'Could not set as head director');
}
$assigner = app(AssignUserToSchool::class);
$assigner(auth()->user(), $school);
}
auth()->user()->refresh();
return redirect('/schools/'.$school->id);
$headSetter->setHeadDirector(auth()->user());
return redirect(route('schools.show', $school));
}
public function show(Request $request, School $school)
@ -118,25 +73,14 @@ class SchoolController extends Controller
return view('schools.edit', ['school' => $school]);
}
public function update(Request $request, School $school)
public function update(SchoolStoreRequest $request, School $school)
{
if ($request->user()->cannot('update', $school)) {
abort(403);
}
request()->validate([
'name' => ['required', 'min:3', 'max:30'],
'address' => ['required'],
'city' => ['required'],
'state' => ['required', 'min:2', 'max:2'],
'zip' => ['required', 'min:5', 'max:10'],
]);
$school->update([
'name' => request('name'),
'address' => request('address'),
'city' => request('city'),
'state' => request('state'),
'zip' => request('zip'),
'name' => $request['name'],
'address' => $request['address'],
'city' => $request['city'],
'state' => $request['state'],
'zip' => $request['zip'],
]);
$message = 'Modified school #'.$school->id.' - '.$school->name.' with address <br>'.$school->address.'<br>'.$school->city.', '.$school->state.' '.$school->zip;
AuditLogEntry::create([
@ -149,46 +93,26 @@ class SchoolController extends Controller
return redirect()->route('schools.show', $school->id)->with('success', 'School details updated');
}
public function my_school()
{
if (Auth::user()->school) {
return redirect('/schools/'.Auth::user()->school->id);
}
return redirect('/schools/create');
}
public function addDirector(School $school)
{
if (auth()->user()->school_id !== $school->id) {
return redirect()->back()->with('error', 'No adding directors to another school');
abort(403);
}
if (! auth()->user()->hasFlag('head_director')) {
return redirect()->back()->with('error', 'Only the head director can add directors to a school');
abort(403);
}
$validData = request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email', 'unique:users'],
'cell_phone' => ['required'],
'judging_preference' => ['required'],
]);
// Generate a random password
$userCreator = app(CreateNewUser::class);
$randomPassword = Str::random(12);
$newUser = User::create([
'first_name' => $validData['first_name'],
'last_name' => $validData['last_name'],
'email' => $validData['email'],
'cell_phone' => $validData['cell_phone'],
'judging_preference' => $validData['judging_preference'],
'password' => Hash::make($randomPassword),
'school_id' => auth()->user()->school_id,
$data = request()->all();
$data['password'] = $randomPassword;
$data['password_confirmation'] = $randomPassword;
$newDirector = $userCreator->create($data);
$newDirector->update([
'school_id' => $school->id,
]);
$logMessage = 'Created user '.$newUser->full_name().' - '.$newUser->email.' as a director at '.$newUser->school->name;
$logAffected = ['users' => [$newUser->id], 'schools' => [$newUser->school_id]];
auditionLog($logMessage, $logAffected);
Mail::to($newUser->email)->send(new NewUserPassword($newUser, $randomPassword));
Mail::to($newDirector->email)->send(new NewUserPassword($newDirector, $randomPassword));
return redirect()->back()->with('success', 'Director added');
}
@ -196,62 +120,49 @@ class SchoolController extends Controller
public function setHeadDirector(School $school, User $user, SetHeadDirector $headSetter)
{
if (auth()->user()->school_id !== $school->id) {
return redirect()->back()->with('error', 'No setting the head director for another school');
abort(403);
}
if (! auth()->user()->hasFlag('head_director')) {
return redirect()->back()->with('error', 'Only the head director can name a new head director');
abort(403);
}
if ($school->id !== $user->school_id) {
return redirect()->back()->with('error', 'The proposed head director must be at your school');
}
try {
$headSetter->setHeadDirector($user);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
abort(403);
}
return redirect()->back()->with('success', 'New head director set');
$headSetter->setHeadDirector($user);
return redirect()->route('schools.show', $school)->with('success', 'New head director set');
}
public function addDomain(School $school)
{
if (auth()->user()->school_id !== $school->id) {
return redirect()->back()->with('error', 'No adding domains for another school');
abort(403);
}
if (! auth()->user()->hasFlag('head_director')) {
return redirect()->back()->with('error', 'Only the head director can add domains');
abort(403);
}
$verifiedData = request()->validate([
'domain' => ['required'],
]);
try {
SchoolEmailDomain::create([
'school_id' => $school->id,
'domain' => $verifiedData['domain'],
]);
} catch (UniqueConstraintViolationException $e) {
return redirect()->back()->with('error', 'That domain is already associated with your school');
}
$logMessage = 'Added domain '.$verifiedData['domain'].' to school '.$school->name;
$logAffected = ['schools' => [$school->id]];
auditionLog($logMessage, $logAffected);
app(AddSchoolEmailDomain::class)->addDomain($school, $verifiedData['domain']);
return redirect()->back()->with('success', 'Domain added');
return redirect()->route('schools.show', $school)->with('success', 'Domain added');
}
public function deleteDomain(SchoolEmailDomain $domain)
{
if (auth()->user()->school_id !== $domain->school_id) {
return redirect()->back()->with('error', 'No deleting domains for another school');
abort(403);
}
if (! auth()->user()->hasFlag('head_director')) {
return redirect()->back()->with('error', 'Only the head director can delete domains');
abort(403);
}
$logMessage = 'Deleted domain '.$domain->domain.' from school '.$domain->school->name;
$logAffected = ['schools' => [$domain->school_id]];
auditionLog($logMessage, $logAffected);
$domain->delete();
return redirect()->back()->with('success', 'Domain deleted');
return redirect()
->route('schools.show', auth()->user()->school)
->with('success', 'Domain deleted');
}
}

View File

@ -2,10 +2,10 @@
namespace App\Http\Controllers;
use App\Actions\Students\CreateStudent;
use App\Http\Requests\StudentStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Student;
use App\Rules\UniqueFullNameAtSchool;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -31,70 +31,24 @@ class StudentController extends Controller
['students' => $students, 'auditions' => $auditions, 'shirtSizes' => $shirtSizes]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
public function store(StudentStoreRequest $request)
{
if ($request->user()->cannot('create', Student::class)) {
abort(403);
}
$request->validate([
'first_name' => ['required'],
'last_name' => [
'required',
new UniqueFullNameAtSchool(request('first_name'), request('last_name'), Auth::user()->school_id),
],
'grade' => ['required', 'integer'],
'shirt_size' => [
'nullable',
function ($attribute, $value, $fail) {
if (! array_key_exists($value, Student::$shirtSizes)) {
$fail("The selected $attribute is invalid.");
}
},
],
]);
$student = Student::create([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
'school_id' => Auth::user()->school_id,
]);
if (request('shirt_size') !== 'none') {
$student->update(['optional_data->shirt_size' => $request['shirt_size']]);
}
$message = 'Created student #'.$student->id.' - '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
$creator = app(CreateStudent::class);
/** @noinspection PhpUnhandledExceptionInspection */
$creator([
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'optional_data' => $request->optional_data,
]);
/** @codeCoverageIgnoreEnd */
return redirect('/students')->with('success', 'Student Created');
}
/**
* Display the specified resource.
*/
public function show(Request $request, Student $student)
{
//
}
/**
* Show the form for editing the specified resource.
*/
@ -112,52 +66,17 @@ class StudentController extends Controller
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Student $student)
public function update(StudentStoreRequest $request, Student $student)
{
if ($request->user()->cannot('update', $student)) {
abort(403);
}
request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
'shirt_size' => [
'nullable',
function ($attribute, $value, $fail) {
if (! array_key_exists($value, Student::$shirtSizes)) {
$fail("The selected $attribute is invalid.");
}
},
],
]);
if (Student::where('first_name', request('first_name'))
->where('last_name', request('last_name'))
->where('school_id', Auth::user()->school_id)
->where('id', '!=', $student->id)
->exists()) {
return redirect()->route('students.edit', $student)->with('error',
'A student with that name already exists at your school.');
}
$student->update([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
]);
$student->update(['optional_data->shirt_size' => $request['shirt_size']]);
$message = 'Updated student #'.$student->id.'<br>Name: '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'optional_data' => $request->optional_data,
]);
return redirect('/students')->with('success', 'Student updated successfully.');
@ -171,16 +90,7 @@ class StudentController extends Controller
if ($request->user()->cannot('delete', $student)) {
abort(403);
}
$message = 'Deleted student #'.$student->id.'<br>Name: '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
]);
$student->delete();
return redirect(route('students.index'));

View File

@ -2,50 +2,64 @@
namespace App\Http\Controllers\Tabulation;
use App\Actions\Tabulation\CalculateAuditionScores;
use App\Actions\Tabulation\RankAuditionEntries;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\EntryFlag;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use function is_null;
class AdvancementController extends Controller
{
protected RankAuditionEntries $ranker;
public function __construct(RankAuditionEntries $ranker)
{
$this->ranker = $ranker;
}
public function status()
{
// Total auditions scores if we haven't done it lately
if (! Cache::has('advancement_status_audition_totaler_throttle')) {
$lock = Cache::lock('advancement_status_audition_totaler_lock');
if ($lock->get()) {
try {
$totaler = app(CalculateAuditionScores::class);
foreach (Audition::forAdvancement()->with('judges')->get() as $audition) {
$totaler($audition);
}
// set throttle
Cache::put('advancement_status_audition_totaler_throttle', true, 15);
} finally {
$lock->release();
}
}
}
$auditions = Audition::forAdvancement()
->with('flags')
->withCount([
'entries' => function ($query) {
$query->where('for_advancement', 1);
$query->where('for_advancement', true);
},
])
->withCount([
'unscoredEntries' => function ($query) {
$query->where('for_advancement', 1);
$query->where('for_advancement', true);
},
])
->orderBy('score_order')
->get();
$auditionData = [];
$auditions->each(function ($audition) use (&$auditionData) {
$scoredPercent = ($audition->entries_count > 0) ?
round((($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count) * 100)
: 100;
$auditions->each(function (Audition $audition) use (&$auditionData) {
$auditionData[] = [
'id' => $audition->id,
'name' => $audition->name,
'entries_count' => $audition->entries_count,
'unscored_entries_count' => $audition->unscored_entries_count,
'scored_entries_count' => $audition->entries_count - $audition->unscored_entries_count,
'scored_percentage' => $scoredPercent,
'scoring_complete' => $audition->unscored_entries_count == 0,
'scored_percentage' => $audition->entries_count > 0 ? ((($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count) * 100) : 0,
'scoring_complete' => $audition->unscored_entries_count === 0,
'published' => $audition->hasFlag('advancement_published'),
];
});
@ -55,29 +69,51 @@ class AdvancementController extends Controller
public function ranking(Request $request, Audition $audition)
{
$entries = $this->ranker->rank('advancement', $audition);
$entries->load('advancementVotes');
$ranker = app(RankAuditionEntries::class);
$entries = $ranker($audition, 'advancement');
$entries->load(['advancementVotes', 'totalScore', 'student.school']);
$scoringComplete = $entries->every(function ($entry) {
return $entry->score_totals[0] >= 0 || $entry->hasFlag('no_show');
$unscoredEntries = $audition->entries()->where('for_advancement', true)->orderBy('draw_number')->get()->filter(function ($entry) {
return ! $entry->totalScore && ! $entry->hasFlag('no_show');
});
return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete'));
$noShowEntries = $audition->entries()->orderBy('draw_number')->get()->filter(function ($entry) {
return $entry->hasFlag('no_show');
});
$scoringComplete = $audition->entries->where('for_advancement', true)->every(function ($entry) {
return $entry->totalScore || $entry->hasFlag('no_show');
});
return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete', 'unscoredEntries', 'noShowEntries'));
}
public function setAuditionPassers(Request $request, Audition $audition)
{
$passingEntries = $request->input('pass');
if (is_null($passingEntries) || count($passingEntries) < 1) {
return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('error',
'Cannot publish advancement if no entries advance');
}
$audition->addFlag('advancement_published');
if (! is_null($passingEntries)) {
$passingEntries = array_keys($passingEntries);
$entries = Entry::whereIn('id', $passingEntries)->get();
foreach ($entries as $entry) {
$entry->addFlag('will_advance');
}
$passEntries = collect(array_keys($passingEntries));
EntryFlag::insert(
$passEntries
->map(fn ($entryId) => [
'entry_id' => $entryId,
'flag_name' => 'will_advance',
'created_at' => now(),
'updated_at' => now(),
])->toArray()
);
}
Cache::forget('audition'.$audition->id.'advancement');
Cache::forget('publicResultsPage');
Cache::forget('rank_advancement_'.$audition->id);
return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success',
'Passers have been set successfully');
@ -86,9 +122,10 @@ class AdvancementController extends Controller
public function clearAuditionPassers(Request $request, Audition $audition)
{
$audition->removeFlag('advancement_published');
foreach ($audition->entries as $entry) {
$entry->removeFlag('will_advance');
}
$audition->entries
->filter(fn ($entry) => $entry->hasFlag('will_advance'))
->each(fn ($entry) => $entry->removeFlag('will_advance'));
Cache::forget('audition'.$audition->id.'advancement');
Cache::forget('publicResultsPage');

View File

@ -1,14 +1,17 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Http\Controllers\Tabulation;
use App\Actions\Tabulation\EnterBonusScore;
use App\Actions\Tabulation\GetBonusScoreRelatedEntries;
use App\Exceptions\ScoreEntryException;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\BonusScore;
use App\Models\Entry;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\DB;
use function request;
@ -73,26 +76,25 @@ class BonusScoreController extends Controller
// Set the new score
try {
$saveBonusScore($judge, $entry, $validData['score']);
} catch (ScoreEntryException $ex) {
} catch (AuditionAdminException $ex) {
DB::rollBack();
return redirect()->route('bonus-scores.entryBonusScoreSheet',
['entry_id' => $entry->id])->with('error', 'Error entering score - '.$ex->getMessage());
}
}
DB::commit();
} catch (\Exception) {
/* @codeCoverageIgnoreStart */
} catch (Exception $ex) {
DB::rollBack();
return redirect()->route('bonus-scores.entryBonusScoreSheet', ['entry_id' => $entry->id])->with('error', 'Error entering score - '.$ex->getMessage());
}
/* @codeCoverageIgnoreEnd */
return redirect()->route('bonus-scores.entryBonusScoreSheet', ['entry_id' => $entry->id])->with('success', 'New bonus score entered');
}
public function destroyBonusScore()
{
}
}

View File

@ -34,7 +34,11 @@ class DoublerDecisionController extends Controller
// $doublerEntry->addFlag('declined');
// }
// }
$this->decider->accept($entry);
try {
$this->decider->accept($entry);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
}
$returnMessage = $entry->student->full_name().' accepted seating in '.$entry->audition->name;
$this->clearCache($entry);

View File

@ -2,16 +2,16 @@
namespace App\Http\Controllers\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\BonusScore;
use App\Models\CalculatedScore;
use App\Models\Entry;
use App\Models\ScoreSheet;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use function to_route;
/**
* Used for tabulation enter noshow menu option
*/
class EntryFlagController extends Controller
{
public function noShowSelect()
@ -33,11 +33,11 @@ class EntryFlagController extends Controller
// If any results are published, get gone
if ($entry->audition->hasFlag('seats_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show for an entry in an audition where seats are published');
'Cannot enter a no-show or failed-prelim for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advancement_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show for an entry in an audition where advancement is published');
'Cannot enter a no-show or failed-prelim for an entry in an audition where advancement is published');
}
if ($entry->hasFlag('no_show')) {
@ -46,6 +46,12 @@ class EntryFlagController extends Controller
$submitRouteName = 'entry-flags.undoNoShow';
$cardHeading = 'Undo No-Show';
$method = 'DELETE';
} elseif ($entry->hasFlag('failed_prelim')) {
$formId = 'no-show-cancellation-form';
$buttonName = 'Remove Failed Prelim';
$submitRouteName = 'entry-flags.undoNoShow';
$cardHeading = 'Undo Failed-Prelim';
$method = 'DELETE';
} else {
$formId = 'no-show-confirmation-form';
$buttonName = 'Confirm No Show';
@ -69,27 +75,16 @@ class EntryFlagController extends Controller
'scores'));
}
/**
* @throws AuditionAdminException
*/
public function enterNoShow(Entry $entry)
{
if ($entry->audition->hasFlag('seats_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advancement_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show for an entry in an audition where advancement is published');
}
DB::table('score_sheets')->where('entry_id', $entry->id)->delete();
$entry->addFlag('no_show');
ScoreSheet::where('entry_id', $entry->id)->delete();
CalculatedScore::where('entry_id', $entry->id)->delete();
BonusScore::where('entry_id', $entry->id)->delete();
if (request()->input('noshow-type') == 'failprelim') {
$msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').';
$entry->addFlag('failed_prelim');
} else {
$msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').';
$recorder = app('App\Actions\Tabulation\EnterNoShow');
try {
$msg = $recorder($entry, request()->input('noshow-type'));
} catch (AuditionAdminException $e) {
return to_route('entry-flags.noShowSelect')->with('error', $e->getMessage());
}
return to_route('entry-flags.noShowSelect')->with('success', $msg);
@ -99,21 +94,32 @@ class EntryFlagController extends Controller
{
if ($entry->audition->hasFlag('seats_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot undo a no-show for an entry in an audition where seats are published');
'Cannot undo a no-show or failed-prelim for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advancement_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot undo a no-show for an entry in an audition where advancement is published');
'Cannot undo a no-show or failed-prelim for an entry in an audition where advancement is published');
}
$entry->removeFlag('no_show');
$entry->removeFlag('failed_prelim');
return to_route('entry-flags.noShowSelect')->with('success',
'No Show status has been removed for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').');
$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.') may now be scored.');
}
public function undoDecline(Entry $entry)
{
if ($entry->audition->hasFlag('seats_published')) {
return redirect()->back()
->with('error', 'Cannot undo a decline for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advancement_published')) {
return redirect()->back()
->with('error', 'Cannot undo a no-show or failed-prelim for an entry in an audition where advancement is published');
}
$entry->removeFlag('declined');
return redirect()->back()->with('success', 'Decline cleared');

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