%PDF- %PDF-
| Direktori : /www/loslex/test/app/Http/Controllers/ |
| Current File : //www/loslex/test/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";
$cacheKey = match(true) {
$contest->date->gte(Carbon::parse("2025-09-01")) => "cup/handgun/2026",
$contest->date->gte(Carbon::parse("2024-09-01")) => "cup/handgun/2025",
default => "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;
}
}