%PDF- %PDF-
Direktori : /www/loslex/demo/app/Http/Controllers/ |
Current File : //www/loslex/demo/app/Http/Controllers/ContestController.php |
<?php namespace App\Http\Controllers; use App\Enums\ContestLevelEnum; use App\Enums\ContestPublicationStatusEnum; use App\Http\Requests\ContestUpdateRequest; use App\Models\Contest; use App\Models\ContestCategory; use App\Models\ContestLevel; use App\Models\OrganizerGroup; use App\Models\Range; use App\Models\Registration; use App\Models\User; use App\Workers\ContestImporter; use App\Workers\RegistrationExporter; use Barryvdh\DomPDF\Facade\Pdf; use Illuminate\Contracts\Foundation\Application; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\LazyCollection; use Illuminate\Support\Str; use Illuminate\View\View; if (!defined('RESULTS_CACHE_TTL')) { define('RESULTS_CACHE_TTL', 7 * 24 * 3600); } class ContestController extends Controller { public function __construct() { $this->middleware('auth')->except(['index', 'show']); } /* Display a listing of the resource. */ public function index(Request $request) : View { $this->authorize('viewAny', Contest::class); return view('contests.list'); } /* Display the specified resource. */ public function show(Contest $contest) : View { $this->authorize('view', $contest); $contest->load(['registrations.user','registrations.division']); return view('contests.show', [ 'contest' => $contest, 'user' => Auth::user(), 'forcenotcompete' => $this->forceNotCompete($contest), 'results' => $this->getResults($contest, $this->needDetailedResults($contest)) ]); } /** * Determines whether the NotCompete flag should be forced in registration form * @param $contest * @return bool * */ private function forceNotCompete(Contest $contest) : bool { $enforcedContestLevels = array( ContestLevelEnum::CUP->value, ContestLevelEnum::CHAMPIONSHIP->value, ContestLevelEnum::SZSCLASSIFICATION->value ); if (!in_array($contest->contest_level_id, $enforcedContestLevels)) { return false; } if (Auth::user()?->is_admin) { return false; } return $contest->registrations ->where('user_id', Auth::user()?->id) ->where('notcomp', 0) ->count() > 0; } /** Show the form for creating a new resource. */ public function create(Request $request) : View { $this->authorize('create', Contest::class); return view('contests.edit', [ 'contest' => null, 'ranges' => Range::where('is_active', 1)->orderBy('name')->get(), 'orggroups' => Auth::user()->is_admin ? OrganizerGroup::orderBy('name')->get() : Auth::user()->organizer_groups->sortBy('name'), 'organizators' => $this->getOrganizerUsers(), 'contestlevels' => ContestLevel::where('is_active', 1)->get(), 'contestcategories' => ContestCategory::where('is_active', 1)->get(), ]); } /** Show the form for editing the specified resource. */ public function edit(Contest $contest): View { $this->authorize('update', $contest); $contestlevels = ContestLevel::where('is_active', 1)->get(); $remdays = $contest->date->diff(Carbon::now()); $contestlevels = $contestlevels->filter(function(ContestLevel $lvl) use ($contest, $remdays) { return $lvl->advance <= $remdays->days || $contest->contest_level_id == $lvl->id; }); return view('contests.edit', [ 'contest' => $contest, 'ranges' => Range::orderBy('name')->get(), 'orggroups' => Auth::user()->is_admin ? OrganizerGroup::orderBy('name')->get() : Auth::user()->organizer_groups->sortBy('name'), 'organizators' => $this->getOrganizerUsers(), 'contestlevels' => $contestlevels, 'contestcategories' => ContestCategory::where('is_active', 1)->get(), ]); } /** Display contest maintenance page */ public function maintain(Contest $contest): View { $this->authorize('update', $contest); $contest->load(['registrations.user','registrations.division', 'registrations.user.registrations']); $contestfiles = LazyCollection::make(function() use ($contest) { $allfiles = Storage::disk('contests')->files($contest->id); foreach ($allfiles as $file) { yield array( 'file' => $file, 'size' => round(Storage::disk('contests')->size($file) / 1024, 2), 'modified' => Carbon::parse(Storage::disk('contests')->lastModified($file)) ); } }); return view('contests.maintain', [ 'contest' => $contest, 'users' => User::get()->sortBy('displayname'), 'contestfiles' => $contestfiles, ]); } /** Toggle publish status of the contest */ public function publish(Contest $contest, int $status): RedirectResponse { $this->authorize('update', $contest); $contest->published = $status; $contest->save(); Log::info("User " . Auth::user()->username . " updated contest publish status {$contest->contestname}", ['changed' => $contest->getChanges()]); return back(); } /** publish contest results */ public function toggleResultPublication(Contest $contest) { $this->authorize('update', $contest); $contest->results_published = !$contest->results_published; $contest->save(); $cacheKey = $contest->date->gte(Carbon::parse("2024-09-01")) ? "cup/handgun/2025" : "cup/handgun/2024"; Cache::forget($cacheKey); Log::info("Cache clear", ['key' => $cacheKey]); Log::info("User " . Auth::user()->username . " updated contest results status {$contest->contestname}", ['changed' => $contest->getChanges()]); return back(); } /** Store a newly created resource in storage. */ public function store(ContestUpdateRequest $request): RedirectResponse { $this->authorize('create', Contest::class); $contestData = new Contest; $contestData->fill($request->validated()); $contestData->fillSettings($request); $contestData->psmatchguid = Str::uuid(); $contestData->save(); Cache::forget('icsContests'); Log::info("Cache clear", ['key' => 'icsContests']); Log::info("User " . Auth::user()->username . " created contest {$contestData->contestname}"); return redirect(route('contest.show', $contestData->id)); } /** Update the specified resource in storage. */ public function update(ContestUpdateRequest $request, Contest $contest): RedirectResponse { $this->authorize('update', $contest); $oldSquadCount = $contest->squads; // store old number of squads $contest->fill($request->validated()); $contest->fillSettings($request); $contest->version++; if ($contest->squads < $oldSquadCount) { Registration::where('contest_id', $contest->id) ->where('squad', '>', $contest->squads) ->update(['squad' => 1]); } $contest->save(); Cache::forget('icsContests'); Log::info("Cache clear", ['key' => 'icsContests']); Log::info("User " . Auth::user()->username . " updated contest {$contest->contestname}", ['contest'=> $contest->id, 'changed' => $contest->getChanges()]); return redirect(route('contest.show', $contest->id)); } /** Remove the specified resource from storage. */ public function destroy(Contest $contest) { } /** Update squad for selected registrations. This is used by organizers when equlizing squads before contest. */ public function changesquad(Contest $contest, Request $request) : RedirectResponse { $this->authorize('update', $contest); if ( is_array($request->selection) && count($request->selection) ) { Registration::whereIn('id', $request->selection)->update(['squad' => $request->squad]); } return back(); } /** Exports TXT files with registreeses email addresses. Scope of the export is determined by $type value. Addresses are formated so it could be copied directly to email address lines. * @param $contest * @param $type Parameter defines the scope of the exported addresses - recognized values are 'all' (all registered), 'contestants' (contestant squads), 'squadr' (referrees/helpers squad), 'builders' (builders), 'paid' (registrees with paid flag). */ public function email(Contest $contest, string $type) { $this->authorize('update', $contest); switch ($type) { default: case "all": $contacts = $contest->registrations->pluck('user.displayname', 'user.email'); break; case "contestants": $contacts = $contest->registrations->where('squad', '>', 0)->pluck('user.displayname', 'user.email'); break; case "squadr": $contacts = $contest->registrations->where('squad', 0)->pluck('user.displayname', 'user.email'); break; case "builders": $contacts = $contest->registrations->where('builder', 1)->pluck('user.displayname', 'user.email'); break; case "notpaid": $contacts = $contest->registrations->where('paid', 0)->pluck('user.displayname', 'user.email'); } $data = "\xEF\xBB\xBF"; //BOM if ($contacts->isNotEmpty()) { foreach ($contacts as $email => $name) { $data .= "$name <$email>; "; } } else { $data .= __('No registrations of this type'); } return response()->streamDownload(function() use ($data) { echo $data; }, "emails_{$contest->id}_$type.txt"); } /** Calls ExportRegistration model class to create export and returns finalized file */ public function export(Contest $contest, string $type) { $this->authorize('update', $contest); $regExport = new RegistrationExporter($contest, $type, request()->has('all')); $headers = array("Content-Type" => $this->getContentType($regExport->filename)); return Storage::disk('contests')->download($regExport->filename, basename($regExport->filename), $headers); } /** Detemines proper ContentType for the exported file.*/ private function getContentType(string $filename) { $pathinfo = pathinfo($filename); switch ($pathinfo['extension']) { case 'csv': return 'application/csv'; case 'json': return 'application/json'; case 'pdf': return 'application/pdf'; case 'psc': return 'application/psc'; case 'xlsx': return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; default: return ''; } } /** Downloads contest-related file from Laravel disk storage */ public function fileDownload(Contest $contest, string $fname) { $this->authorize('fileManage', $contest); $headers = array("Content-Type" => $this->getContentType($fname)); return Storage::disk('contests')->download($contest->id . "/" . $fname, $fname, $headers); } /** Deletes specified contest-related file from the storage */ public function fileDelete(Contest $contest, string $fname) : RedirectResponse { $this->authorize('fileManage', $contest); Storage::disk('contests')->delete($contest->id . "/" . $fname); return redirect(route('contest.maintain', $contest->id) . '?export'); } /** Stores and processes imported contest results file */ public function processImport(Request $request, Contest $contest) : Application|\Illuminate\Foundation\Application|RedirectResponse|View { $this->authorize('update', $contest); $file = $request->file('file'); $extension = $file->getClientOriginalExtension(); $uploadName = $contest->id . "/import-" . date("Y-m-d_H-i-s") . "." . $extension; Storage::disk('contests')->put($uploadName, $file->getContent()); $importer = new ContestImporter($contest, $uploadName, $extension); if($importer->hasMessages()) { return view('contests.import-messages', [ 'importer' => $importer, 'contestId' => $contest->id ]); } return redirect(route('contest.show', $contest->id) . "#anresults"); } /** Displays print-ready page with all/present contestants. This can be used to collect signatures form shooters and isnerted in to the range visitors book afterwards. */ public function presentation(Contest $contest) { $this->authorize('update', $contest); $regs = request()->has('all') ? $contest->registrations : $contest->registrations->where('present', 1); $regs = $regs->sortBy(['squad', 'user.lastname']); $data = array( 'contest' => $contest, 'registrations' => $regs, 'freeSlots' => $contest->capacity < 50 ? 5 : 10 ); return view("contests.presentation", $data); } /** Displays detailed contest results */ public function detailedResults(Contest $contest) { $this->authorize('view', $contest); if(request()->has('print')) { $pdfname = $contest->date->isoFormat("YYYY-MM-DD") . "_" . str_replace("/", "_", str_replace(".", "-", Str::ucfirst(Str::camel(Str::ascii($contest->contestname))))) . "-detail.pdf"; if (!Storage::disk('contests')->exists($contest->id . "/" . $pdfname)) { $results = $this->getResults($contest, true); $width = 500 + $contest->stages * 300; $height = 100; $divisions = array(); if ($contest->isLosik()) { $width = 1550; $height = 170 + count($results['divisions']['All']->shooters) * 15; $divisions = ['All' => $results['divisions']['All']]; } else { foreach ($results['divisions'] as $division) { if ($division->name == "All") { continue; } $height += 60 + count($division->shooters) * 15; $divisions[$division->name] = $division; } } $results['divisions'] = $divisions; $papersize = [0, 0, $width, $height]; $pdf = PDF::loadView("contests.results.detail-print", $results)->setPaper($papersize); Storage::disk('contests')->put($contest->id . "/" . $pdfname, $pdf->stream($pdfname)); } return Storage::disk('contests')->download($contest->id . "/" . $pdfname, $pdfname, ["Content-Type" => $this->getContentType($pdfname)]); } return view("contests.results.detail", $this->getResults($contest, true)); } /** Displays basic contest results overview */ public function overviewResults(Contest $contest) { $this->authorize('view', $contest); if(request()->has('print')) { $pdfname = $contest->date->isoFormat("YYYY-MM-DD") . "_" . str_replace("/", "_", str_replace(".", "-", Str::ucfirst(Str::camel(Str::ascii($contest->contestname))))) . "-overview.pdf"; if(!Storage::disk('contests')->exists($contest->id . "/" . $pdfname)) { $template = "contests.results.overview-print"; $results = $this->getResults($contest, $this->needDetailedResults($contest)); $width = 220 + $contest->stages * 60; $height = 100; $divisions = array(); $overridePaperSize = false; if ($contest->isLosik()) { $width = 500; $height = 170 + count($results['divisions']['All']->shooters) * 15; $divisions = ['All' => $results['divisions']['All']]; } elseif($contest->contest_level_id == ContestLevelEnum::SZSCLASSIFICATION->value) { $divisions = ['All' => $results['divisions']['All']]; $template = "contests.results.szclassification-print"; $overridePaperSize = "a4"; } else { foreach ($results['divisions'] as $division) { if ($division->name == "All") { continue; } $height += 60 + count($division->shooters) * 15; $divisions[$division->name] = $division; } } $results['divisions'] = $divisions; $papersize = $overridePaperSize ? $overridePaperSize : [0, 0, $width, $height]; $pdf = PDF::loadView($template, $results) ->setPaper($papersize); Storage::disk('contests')->put($contest->id . "/" . $pdfname, $pdf->stream($pdfname)); } return Storage::disk('contests')->download($contest->id . "/" . $pdfname, $pdfname, ["Content-Type" => "application/pdf"]); } } /** Displays a simplified table with each division winners */ public function ceremonies(Contest $contest) { $this->authorize('view', $contest); if(request()->has('print')) { $pdfname = $contest->date->isoFormat("YYYY-MM-DD") . "_" . str_replace("/", "_", str_replace(".", "-", Str::ucfirst(Str::camel(Str::ascii($contest->contestname))))) . "-awards.pdf"; if(!Storage::disk('contests')->exists($contest->id . "/" . $pdfname)) { $results = $this->getResultsCeremony($contest); $pdf = PDF::loadView("contests.results.ceremonies", $results) ->setPaper('a4'); Storage::disk('contests')->put($contest->id . "/" . $pdfname, $pdf->stream($pdfname)); } return Storage::disk('contests')->download($contest->id . "/" . $pdfname, $pdfname, ["Content-Type" => "application/pdf"]); } return view("contests.results.ceremonies", $this->getResultsCeremony($contest)); } private function getResults(Contest $contest, bool $detailed) { $cacheKey = $contest->getCacheKeyPrefix('results') . ($detailed ? "detail" : "overview"); return Cache::remember($cacheKey, RESULTS_CACHE_TTL, function() use ($detailed, $contest, $cacheKey) { $results = DB::select("SELECT * FROM `contest_results` WHERE `contest_id`=?", [$contest->id]); if (!$results) { return null; } Log::info("Cache MISS", ['key' => $cacheKey]); // Prepare empty arrays with detailed results $stages = array(); $divisions = array(); foreach ($results as $result) { if (!isset($divisions[$result->bgdivision])) { $divisions[$result->bgdivision] = new \stdClass(); $divisions[$result->bgdivision]->name = $result->bgdivision; $divisions[$result->bgdivision]->shooters = array(); } $divisions[$result->bgdivision]->shooters[$result->registration_id] = new \stdClass(); $divisions[$result->bgdivision]->shooters[$result->registration_id]->percent = number_format($result->percent, 2); $divisions[$result->bgdivision]->shooters[$result->registration_id]->name = $result->lastname . " " . $result->firstname . ($result->namesuffix ? " " . $result->namesuffix : ""); $divisions[$result->bgdivision]->shooters[$result->registration_id]->anonname = implode(" ", array_filter([Str::mask($result->lastname, '*', 1), Str::mask($result->firstname, '*', 1), Str::mask($result->namesuffix, '*', 1)])); $divisions[$result->bgdivision]->shooters[$result->registration_id]->registrationId = $result->registration_id; $divisions[$result->bgdivision]->shooters[$result->registration_id]->origDivision = $result->origdivision; $divisions[$result->bgdivision]->shooters[$result->registration_id]->notcomp = $result->notcomp; $divisions[$result->bgdivision]->shooters[$result->registration_id]->dq = $result->dq; $divisions[$result->bgdivision]->shooters[$result->registration_id]->stages = array(); } foreach (DB::select("SELECT * FROM `stage_results` WHERE `contest_id`=?", [$contest->id]) as $result) { $divisions[$result->bgdivision]->shooters[$result->registration_id]->stages[$result->order] = $result; } if ($detailed) { foreach (DB::select("SELECT `id`, `strings`, `order` FROM `stages` WHERE `contest_id`=?", [$contest->id]) as $stg) { $stages[$stg->order] = new \stdClass(); $stages[$stg->order]->strings = $stg->strings; $stages[$stg->order]->id = $stg->id; $stages[$stg->order]->span = 11 + ($stg->strings > 1 ? $stg->strings : 0); } foreach (DB::select("SELECT `order`, `time`, `bgdivision`, `registration_id`, `storder` FROM `stage_times` WHERE `contest_id`=?", [$contest->id]) as $str) { if (!isset($divisions[$str->bgdivision]->shooters[$str->registration_id]->stageStrings[$str->storder])) { $divisions[$str->bgdivision]->shooters[$str->registration_id]->stageStrings[$str->storder] = array(); } $divisions[$str->bgdivision]->shooters[$str->registration_id]->stageStrings[$str->storder][$str->order] = $str->time; } } $divisions = $this->sortResults($divisions); return array( "divisions" => $divisions, "numStages" => $contest->stages, "name" => $contest->contestname, "date" => $contest->date, "stages" => $stages ); }); } private function getOrganizerUsers() { if (Auth::user()->is_admin) { $organizers = User::get()->sortBy('displayname'); } else { $organizers = Collection::make(); foreach (Auth::user()->organizer_groups as $group) { $organizers = $organizers->merge($group->users); } $organizers = $organizers->unique('id')->sortBy('displayname'); } return $organizers; } private function getResultsCeremony(Contest $contest) { $cacheKey = $contest->getCacheKeyPrefix('results') . 'ceremony'; return Cache::remember($cacheKey, RESULTS_CACHE_TTL, function() use ($contest, $cacheKey) { Log::info("Cache MISS", ['key' => $cacheKey]); $results = DB::select("SELECT firstname, lastname, namesuffix, percent, bgdivision FROM `contest_results` WHERE `contest_id`=:contestId AND notcomp=0 and bgdivision collate utf8mb4_general_ci <> 'All'", ['contestId' => $contest->id]); if (!$results) { return null; } $divisions = array(); foreach ($results as $result) { if (!isset($divisions[$result->bgdivision])) { $divisions[$result->bgdivision] = new \stdClass(); $divisions[$result->bgdivision]->name = $result->bgdivision; $divisions[$result->bgdivision]->shooters = array(); } $shooter = new \stdClass(); $shooter->percent = number_format($result->percent, 2); $shooter->name = $result->lastname . " " . $result->firstname . ($result->namesuffix ? " " . $result->namesuffix : ""); $divisions[$result->bgdivision]->shooters[] = $shooter; } $divisions = $this->sortResults($divisions); return array( "divisions" => $divisions, "name" => $contest->contestname, "date" => $contest->date, ); }); } private function sortResults(array $divisions): array { foreach ($divisions as $div) { uasort($div->shooters, function ($a, $b) { if ($a->percent < $b->percent) { return 1; } if ($a->percent == $b->percent && $a->name < $b->name) { return 1; } return -1; }); } uasort($divisions, function ($a, $b) { $divPrio = array( "ALL" => 1, "PI" => 2, "KPI" => 3, "MPI" => 4, "RE" => 5, "MRE" => 6, "OPT" => 7, "PDW" => 8, "PSA" => 9, "POP" => 10, "BSA" => 11, "BOP" => 12, "BOS" => 13 ); return $divPrio[strtoupper($a->name)] > $divPrio[strtoupper($b->name)] ? 1 : -1; }); return $divisions; } private function needDetailedResults(Contest $contest) { return $contest->contest_level_id == ContestLevelEnum::SZSCLASSIFICATION->value; } }