diff --git a/app/Events/ScoringGuideChange.php b/app/Events/ScoringGuideChange.php new file mode 100644 index 0000000..b265ad9 --- /dev/null +++ b/app/Events/ScoringGuideChange.php @@ -0,0 +1,36 @@ + + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('channel-name'), + ]; + } +} diff --git a/app/Listeners/RefreshScoringGuideCache.php b/app/Listeners/RefreshScoringGuideCache.php new file mode 100644 index 0000000..4d6e7d3 --- /dev/null +++ b/app/Listeners/RefreshScoringGuideCache.php @@ -0,0 +1,28 @@ +scoreService = $scoreService; + } + + /** + * Handle the event. + */ + public function handle(ScoringGuideChange $event): void + { + $this->scoreService->clearScoringGuideCache(); + } +} diff --git a/app/Observers/ScoringGuideObserver.php b/app/Observers/ScoringGuideObserver.php index 3f63e7c..bd5a203 100644 --- a/app/Observers/ScoringGuideObserver.php +++ b/app/Observers/ScoringGuideObserver.php @@ -3,6 +3,7 @@ namespace App\Observers; use App\Events\AuditionChange; +use App\Events\ScoringGuideChange; use App\Models\ScoringGuide; class ScoringGuideObserver @@ -12,7 +13,7 @@ class ScoringGuideObserver */ public function created(ScoringGuideObserver $scoringGuide): void { - // + ScoringGuideChange::dispatch(); } /** @@ -21,6 +22,7 @@ class ScoringGuideObserver public function updated(ScoringGuideObserver $scoringGuide): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } /** @@ -29,6 +31,7 @@ class ScoringGuideObserver public function deleted(ScoringGuideObserver $scoringGuide): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } /** @@ -37,6 +40,7 @@ class ScoringGuideObserver public function restored(ScoringGuideObserver $scoringGuide): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } /** @@ -45,5 +49,6 @@ class ScoringGuideObserver public function forceDeleted(ScoringGuideObserver $scoringGuide): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } } diff --git a/app/Observers/SubscoreDefinitionObserver.php b/app/Observers/SubscoreDefinitionObserver.php index 43397e1..ae54dec 100644 --- a/app/Observers/SubscoreDefinitionObserver.php +++ b/app/Observers/SubscoreDefinitionObserver.php @@ -3,6 +3,7 @@ namespace App\Observers; use App\Events\AuditionChange; +use App\Events\ScoringGuideChange; use App\Models\SubscoreDefinition; class SubscoreDefinitionObserver @@ -13,6 +14,7 @@ class SubscoreDefinitionObserver public function created(SubscoreDefinition $subscoreDefinition): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } /** @@ -21,6 +23,7 @@ class SubscoreDefinitionObserver public function updated(SubscoreDefinition $subscoreDefinition): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } /** @@ -29,6 +32,7 @@ class SubscoreDefinitionObserver public function deleted(SubscoreDefinition $subscoreDefinition): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } /** @@ -37,6 +41,7 @@ class SubscoreDefinitionObserver public function restored(SubscoreDefinition $subscoreDefinition): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } /** @@ -45,5 +50,6 @@ class SubscoreDefinitionObserver public function forceDeleted(SubscoreDefinition $subscoreDefinition): void { AuditionChange::dispatch(); + ScoringGuideChange::dispatch(); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 67c15ea..e8d74c2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,7 +3,11 @@ namespace App\Providers; use App\Events\AuditionChange; +use App\Events\EntryChange; +use App\Events\ScoringGuideChange; use App\Listeners\RefreshAuditionCache; +use App\Listeners\RefreshEntryCache; +use App\Listeners\RefreshScoringGuideCache; use App\Models\Audition; use App\Models\Entry; use App\Models\Room; @@ -27,6 +31,7 @@ use App\Observers\UserObserver; use App\Services\AuditionCacheService; use App\Services\DoublerService; use App\Services\EntryCacheService; +use App\Services\ScoreService; use App\Services\TabulationService; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; @@ -46,6 +51,10 @@ class AppServiceProvider extends ServiceProvider return new EntryCacheService($app->make(AuditionCacheService::class)); }); + $this->app->singleton(ScoreService::class, function($app) { + return new ScoreService($app->make(AuditionCacheService::class), $app->make(EntryCacheService::class)); + }); + $this->app->singleton(TabulationService::class, function($app) { return new TabulationService($app->make(AuditionCacheService::class)); }); @@ -79,5 +88,15 @@ class AppServiceProvider extends ServiceProvider AuditionChange::class, RefreshAuditionCache::class ); + + Event::listen( + EntryChange::class, + RefreshEntryCache::class + ); + + Event::listen( + ScoringGuideChange::class, + RefreshScoringGuideCache::class + ); } } diff --git a/app/Services/EntryCacheService.php b/app/Services/EntryCacheService.php index 3324028..901a075 100644 --- a/app/Services/EntryCacheService.php +++ b/app/Services/EntryCacheService.php @@ -17,6 +17,12 @@ class EntryCacheService $this->auditionCache = $auditionCache; } + /** + * Return a collection of all entries for the provided auditionId along with the + * student.school for each entry. + * @param $auditionId + * @return \Illuminate\Database\Eloquent\Collection + */ public function getEntriesForAudition($auditionId) { $cacheKey = 'audition' . $auditionId . 'entries'; @@ -28,7 +34,13 @@ class EntryCacheService }); } - public function getEntries(): Collection + /** + * Returns a collection of collections of entries, one collection for each audition. + * The outer collection is keyed by the audition ID. The included entries are + * with their student.school. + * @return Collection + */ + public function getAllEntriesByAudition(): Collection { $auditions = $this->auditionCache->getAuditions(); $allEntries = []; @@ -38,10 +50,19 @@ class EntryCacheService return collect($allEntries); } + public function getAllEntries() + { + $cacheKey = 'allEntries'; + return Cache::remember($cacheKey, 3600, function() { + return Entry::with(['student.school'])->get()->keyBy('id'); + }); + } + public function clearEntryCacheForAudition($auditionId): void { $cacheKey = 'audition' . $auditionId . 'entries'; Cache::forget($cacheKey); + Cache::forget('allEntries'); } public function clearEntryCaches(): void diff --git a/app/Services/ScoreService.php b/app/Services/ScoreService.php new file mode 100644 index 0000000..e6b1f1e --- /dev/null +++ b/app/Services/ScoreService.php @@ -0,0 +1,123 @@ +auditionCache = $auditionCache; + $this->entryCache = $entryCache; + } + + /** + * Cache all scoring guides + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getScoringGuides(): \Illuminate\Database\Eloquent\Collection + { + $cacheKey = 'scoringGuides'; + return Cache::remember($cacheKey, 3600, fn() => ScoringGuide::with('subscores')->get()); + } + + /** + * Retrieve a single scoring guide from the cache + * @param $id + * @return ScoringGuide + */ + public function getScoringGuide($id): ScoringGuide + { + return $this->getScoringGuides()->firstWhere('id', $id); + } + + /** + * Clear the scoring guide cache + * @return void + */ + public function clearScoringGuideCache(): void + { + Cache::forget('scoringGuides'); + } + + /** + * Get final scores array for the requested entry. The first element is the total score. The following elements are sums + * of each subscore in tiebreaker order + * @param Entry $entry + * @return array + */ + public function entryTotalScores(Entry $entry) + { + $cacheKey = 'entry' . $entry->id . 'totalScores'; + return Cache::remember($cacheKey, 3600, fn() => $this->calculateFinalScoreArray($entry->audition->scoring_guide_id, $entry->scoreSheets)); + } + + /** + * Calculate and cache scores for all entries for the provided audition ID + * @param $auditionId + * @return void + */ + public function calculateScoresForAudition($auditionId) + { + $audition = $this->auditionCache->getAudition($auditionId); + $scoringGuideId = $audition->scoring_guide_id; + $entries = Entry::where('audition_id',$auditionId)->with('scoreSheets')->get(); + + foreach ($entries as $entry) { + $cacheKey = 'entry' . $entry->id . 'totalScores'; + if (Cache::has($cacheKey)) continue; + $thisTotalScore = $this->calculateFinalScoreArray($scoringGuideId, $entry->scoreSheets); + Cache::put($cacheKey,$thisTotalScore,3600); + } + } + + /** + * Calculate final score using teh provided scoring guide and score sheets. Returns an array of scores + * The first element is the total score. The following elements are the sum of each subscore + * in tiebreaker order. + * @param $scoringGuideId + * @param array|Collection $scoreSheets + * @return array + */ + public function calculateFinalScoreArray($scoringGuideId, array|Collection $scoreSheets): array + { + + $sg = $this->getScoringGuide($scoringGuideId); + + // TODO cache the scoring guides with their subscores + $subscores = $sg->subscores->sortBy('tiebreak_order'); + + // Init final scores array + $finalScoresArray = []; + foreach ($subscores as $subscore) { + $finalScoresArray[$subscore->id] = 0; + } + + foreach($scoreSheets as $sheet) { + foreach ($sheet->subscores as $ss) { + $finalScoresArray[$ss['subscore_id']] += $ss['score']; + } + } + + // calculate weighted final score + $totalScore = 0; + $totalWeight = 0; + foreach ($subscores as $subscore) { + $totalScore += ($finalScoresArray[$subscore->id] * $subscore->weight); + $totalWeight += $subscore->weight; + } + $totalScore = ($totalScore / $totalWeight); + array_unshift($finalScoresArray,$totalScore); + return $finalScoresArray; + } +} diff --git a/composer.json b/composer.json index 5e5dc55..d2e6d19 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "laravel/fortify": "^1.21", "laravel/framework": "^11.0", "laravel/tinker": "^2.9", + "predis/predis": "^2.2", "symfony/http-client": "^7.1", "symfony/mailgun-mailer": "^7.1" }, diff --git a/composer.lock b/composer.lock index fb96672..e72f2be 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "698dd33af37d56773475a28ad6825a83", + "content-hash": "621017524a521b75138e8a946978fb42", "packages": [ { "name": "bacon/bacon-qr-code", @@ -2679,6 +2679,67 @@ }, "time": "2022-06-13T21:57:56+00:00" }, + { + "name": "predis/predis", + "version": "v2.2.2", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ~9.4.4" + }, + "suggest": { + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Till Krüss", + "homepage": "https://till.im", + "role": "Maintainer" + } + ], + "description": "A flexible and feature-complete Redis client for PHP.", + "homepage": "http://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v2.2.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2023-09-13T16:42:03+00:00" + }, { "name": "psr/clock", "version": "1.0.0", diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000..8f0da75 Binary files /dev/null and b/dump.rdb differ diff --git a/resources/views/test.blade.php b/resources/views/test.blade.php index de0e37c..832aa02 100644 --- a/resources/views/test.blade.php +++ b/resources/views/test.blade.php @@ -4,24 +4,23 @@ use App\Models\SchoolEmailDomain; use App\Models\ScoreSheet; use App\Models\ScoringGuide; - use App\Models\User;use App\Services\AuditionCacheService;use App\Settings; + use App\Models\User;use App\Services\ScoreService;use App\Settings; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Session; @endphp -@inject('entryservice','App\Services\EntryCacheService'); +@inject('scoreservice','App\Services\ScoreService'); Test Page @php - $e = $entryservice->getEntries(); - dd($e); + $entry = Entry::first(); + $scoreservice->calculateScoresForAudition(6); @endphp + Entry 392 scores: {{ $scoreservice->entryTotalScores(Entry::find(392))[0] }} + + - @foreach($auditions as $audition) - {{ $audition->name }} has {{ $audition->entries_count }} entries. {{ $audition->scored_entries_count }} are - fully scored.
- @endforeach
diff --git a/routes/web.php b/routes/web.php index c5b9a2d..cfc8ace 100644 --- a/routes/web.php +++ b/routes/web.php @@ -15,7 +15,9 @@ use App\Http\Middleware\CheckIfCanJudge; use App\Http\Middleware\CheckIfCanTab; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; - +Route::get('rediscli', function() { + return \Illuminate\Support\Facades\Redis::ping(); +}); Route::get('/test',[TestController::class,'flashTest'])->middleware('auth','verified'); Route::view('/','welcome')->middleware('guest')->name('home');