%PDF- %PDF-
Direktori : /www/loslex/production/app/Workers/ |
Current File : /www/loslex/production/app/Workers/ContestImporter.php |
<?php namespace App\Workers; use App\Enums\ContestLevelEnum; use App\Http\Controllers\CupResultsController; use App\Models\Contest; use App\Models\ContestDivision; use App\Models\Registration; use App\Models\ShooterStage; use App\Models\ShooterStageTarget; use App\Models\ShooterStageTime; use App\Models\Stage; use App\Models\User; use App\Rules\ForbiddenUsernames; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Validation\Rule; use PhpOffice\PhpSpreadsheet\Reader\Xls; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use Ramsey\Uuid\Uuid; use function Psy\sh; use stdClass; use ZipArchive; class ContestImporter { public array $errors = array(); public array $warnings = array(); public function __construct(Contest $contest, string $filename, string $extension) { switch ($extension){ case 'xls': $this->importExcel($contest, $filename); break; case 'xlsx': $this->importExcel($contest, $filename, true); break; case 'psc': $this->importPractiScore($contest, $filename); break; default: $this->errors[] = __("File with extension :ext is not valid PractiScore or Excel file. Please import only .psc/.xls/.xlsx files.", ['ext' => $extension]); return; } $this->updateMaterializedViews($contest); $this->clearContestCaches($contest); $this->deleteContestPDFs($contest); $this->clearCupCache($contest); } public static function updateMaterializedViews(Contest $contest) { // Update materialized views DB::statement("DELETE FROM `shooter_stage_results` WHERE `contest_id`=?", [$contest->id]); DB::statement("DELETE FROM `contest_results` WHERE `contest_id`=?", [$contest->id]); DB::statement("INSERT INTO `shooter_stage_results` (SELECT * FROM `w_shooter_stage_results` WHERE `contest_id`=?)", [$contest->id]); DB::statement("INSERT INTO `contest_results` (SELECT * FROM `w_contest_results` WHERE `contest_id`=?)", [$contest->id]); } public static function clearContestCaches(Contest $contest) { // Remove results cache Cache::forget($contest->getCacheKeyPrefix("results") . "detail"); Log::info("Cache clear", ['key' => $contest->getCacheKeyPrefix("results") . "detail"]); Cache::forget($contest->getCacheKeyPrefix("results") . "overview"); Log::info("Cache clear", ['key' => $contest->getCacheKeyPrefix("results") . "overview"]); Cache::forget($contest->getCacheKeyPrefix("results") . "ceremony"); Log::info("Cache clear", ['key' => $contest->getCacheKeyPrefix("results") . "ceremony"]); } public static function clearCupCache(Contest $contest) { if ($contest->contest_level_id == ContestLevelEnum::CUP->value) { $cacheKey = $contest->date->gte(Carbon::parse("2024-09-01")) ? "cup/handgun/2025" : "cup/handgun/2024"; // Remove cup cache Cache::forget($cacheKey); Log::info("Cache clear", ['key' => $cacheKey]); } } public static function deleteContestPDFs(Contest $contest) { // Delete PDFs from storage $pdfname = $contest->date->isoFormat("YYYY-MM-DD") . "_" . str_replace(".", "-", Str::ucfirst(Str::camel(Str::ascii($contest->contestname)))); Storage::disk('contests')->delete( [ $contest->id . "/" . $pdfname . "-overview.pdf", $contest->id . "/" . $pdfname . "-detail.pdf", $contest->id . "/" . $pdfname . "-awards.pdf" ]); } protected function importExcel(Contest $contest, string $filename, bool $isXlsx = false): void { $numStages = $contest->stages; $tmpName = tempnam(sys_get_temp_dir(), 'practiscore.xls'); file_put_contents($tmpName, Storage::disk('contests')->get($filename)); $reader = $isXlsx ? new Xlsx() : new Xls(); $spreadsheet = $reader->load($tmpName); $ss = $spreadsheet->setActiveSheetIndex(0); DB::beginTransaction(); $importData = array(); $rowIterator = $ss->getRowIterator(3); $divisionId = 0; while ($rowIterator->valid()) { $rowIterator->next(); $row = $rowIterator->current(); $position = $row->getRowIndex(); $cCell = trim($ss->getCell("C" . $position)->getValue()); if(!$cCell || $cCell == "Jméno") { continue; } if(isset($this->textDivisions[$cCell])) { // Prepare division ID $divisionId = ContestDivision::where(DB::raw('UPPER(bgdivision)'), $this->textDivisions[$cCell])->first()->id; continue; } // Get mail and alias $bCellExploded = explode("/", trim($ss->getCell("B" . $position)->getValue())); $email = isset($bCellExploded[0]) ? trim($bCellExploded[0]) : ""; $alias = isset($bCellExploded[1]) ? trim($bCellExploded[1]) : ""; $regGuid = isset($bCellExploded[2]) ? trim($bCellExploded[2]) : ""; $notCompeting = strtoupper(trim($ss->getCell("A" . $position)->getValue())) == "MZ"; $dq = strtoupper(trim($ss->getCell("A" . $position)->getValue())) == "DQ"; $registration = $contest->registrations()->where('canceltoken', $regGuid)->first(); if(!$registration) { $alias = strtoupper($alias); list($lastName, $firstName) = explode(" ", $ss->getCell("C" . $position)->getValue()); $user = $this->getOrCreateUserByAliasOrEmail($alias, $email, $firstName, $lastName); if(!$user) { continue; } $registration = new Registration([ 'contest_id' => $contest->id, 'user_id' => $user->id, 'contest_division_id' => $divisionId, 'squad' => 1, 'notcomp' => $notCompeting, 'dq' => $dq ]); $registration->canceltoken = Str::orderedUuid(); } $registration->contest_division_id = $divisionId; $registration->dq = $dq; $registration->notcomp = $notCompeting; $shooter = new stdClass(); $shooter->registration = $registration; $shooter->stages = array(); // Get stage data $col = 4; for ($i = 1; $i <= $numStages; $i++) { $stage = new stdClass(); $stage->order = $i; $stage->proc = $ss->getCell([$col + 8, $position])->getValue() ?? 0; $stage->noshoot = $ss->getCell([$col + 7, $position])->getValue() ?? 0; $target = new stdClass(); $target->order = 1; $target->popper = $ss->getCell([$col + 1, $position])->getValue() ?? 0; $target->alpha = $ss->getCell([$col + 2, $position])->getValue() ?? 0; $target->charlie = $ss->getCell([$col + 3, $position])->getValue() ?? 0; $target->delta = $ss->getCell([$col + 4, $position])->getValue() ?? 0; $target->miss = $ss->getCell([$col + 5, $position])->getValue() ?? 0; $target->misspopper = $ss->getCell([$col + 6, $position])->getValue() ?? 0; $time = new stdClass(); $time->time = $ss->getCell([$col, $position])->getValue(); $time->order = 1; $time->string = null; $stage->targets = [$target]; $stage->times = [$time]; $shooter->stages[] = $stage; $col += 10; } $importData[] = $shooter; } $json = json_encode($importData, JSON_PRETTY_PRINT); Storage::disk('contests')->put($filename . ".json", $json); if(count($this->errors) > 0) { // Do not proceed if there are errors in the imported file return; } // Delete old data before import DB::statement("DELETE FROM shooter_stage_times WHERE shooter_stage_id in (SELECT id FROM shooter_stages where stage_id in (SELECT id FROM stages WHERE contest_id = ?))", [$contest->id]); DB::statement("DELETE FROM shooter_stage_targets WHERE shooter_stage_id in (SELECT id FROM shooter_stages where stage_id in (SELECT id FROM stages WHERE contest_id = ?))", [$contest->id]); DB::statement("DELETE FROM shooter_stages WHERE stage_id in (SELECT id FROM stages WHERE contest_id = ?)", [$contest->id]); DB::statement("DELETE FROM stages WHERE contest_id = ?", [$contest->id]); // Create stages $stageIds = array(); for($i = 1; $i <= $numStages; $i++) { $st = new Stage([ 'contest_id' => $contest->id, 'name' => "Stage $i", 'order' => $i, 'strings' => 1 ]); $st->save(); $stageIds[$i] = $st->id; } foreach ($importData as $shooter) { $shooter->registration->save(); foreach ($shooter->stages as $stage) { $shooterStage = new ShooterStage([ 'registration_id' => $shooter->registration->id, 'stage_id' => $stageIds[$stage->order], 'proc' => $stage->proc, 'noshoot' => $stage->noshoot ]); $shooterStage->save(); foreach ($stage->times as $time) { $shooterStageTime = new ShooterStageTime([ 'shooter_stage_id' => $shooterStage->id, 'time' => $time->time, 'order' => $time->order, 'string' => $time->string ]); $shooterStageTime->save(); } foreach ($stage->targets as $target) { $shooterStageTarget = new ShooterStageTarget([ 'shooter_stage_id' => $shooterStage->id, 'order' => $target->order, 'popper' => $target->popper, 'alpha' => $target->alpha, 'charlie' => $target->charlie, 'delta' => $target->delta, 'miss' => $target->miss, 'misspopper' => $target->misspopper ]); $shooterStageTarget->save(); } } } DB::commit(); } protected function importPractiScore(Contest $contest, string $filename): void { $tmpName = tempnam(sys_get_temp_dir(), 'practiscore.zip'); file_put_contents($tmpName, Storage::disk('contests')->get($filename)); $zip = new ZipArchive; $zip->open($tmpName); $txt = $zip->getFromName('match_def.json'); $matchDef = json_decode($txt); $txt = $zip->getFromName('match_scores.json'); $scores = json_decode($txt); unset($txt); $zip->close(); unlink($tmpName); // Prepare stages $stages = array(); $stageOrderMap = array(); $numStages = 0; foreach ($matchDef->match_stages as $match_stage) { $stage = new stdClass(); $stage->guid = $match_stage->stage_uuid; $stage->order = $match_stage->stage_number; $stage->name = $match_stage->stage_name; $stage->deleted = $match_stage->stage_deleted ?? false; $stage->strings = $match_stage->stage_strings; $stages[$stage->guid] = $stage; if (!$stage->deleted) { $numStages++; $stageOrderMap[$stage->order] = $stage->order; } } if($contest->stages != $numStages) { $this->errors[] = __('Contest has :cStages but imported PractiScore file contains :psStages stages', ['cStages' => $contest->stages, 'psStages' => $numStages]); return; } // reorder stages (to remap indexes of missing stages) $stageOrderMap = array_values($stageOrderMap); array_unshift($stageOrderMap, 0); DB::beginTransaction(); $shooters = $this->psProcessShooters($matchDef, $contest); if($shooters == null) { DB::rollBack(); return; } DB::statement("DELETE FROM shooter_stage_times WHERE shooter_stage_id in (SELECT id FROM shooter_stages where stage_id in (SELECT id FROM stages WHERE contest_id = ?))", [$contest->id]); DB::statement("DELETE FROM shooter_stage_targets WHERE shooter_stage_id in (SELECT id FROM shooter_stages where stage_id in (SELECT id FROM stages WHERE contest_id = ?))", [$contest->id]); DB::statement("DELETE FROM shooter_stages WHERE stage_id in (SELECT id FROM stages WHERE contest_id = ?)", [$contest->id]); DB::statement("DELETE FROM stages WHERE contest_id = ?", [$contest->id]); // Process scores foreach ($scores->match_scores as $stage) { $defStage = $stages[$stage->stage_uuid]; if ($defStage->deleted) { continue; } $st = new Stage([ 'contest_id' => $contest->id, 'name' => $defStage->name, 'order' => array_search($defStage->order, $stageOrderMap, true), 'strings' => $defStage->strings ]); $st->save(); $stageId = $st->id; foreach ($stage->stage_stagescores as $score) { if (!isset($shooters[$score->shtr])) { continue; } $shooterStage = new ShooterStage([ 'stage_id' => $stageId, 'registration_id' => $shooters[$score->shtr]->id, 'proc' => $score->proc ?? 0, 'noshoot' => 0 ]); $shooterStage->save(); $stageNo = $shooterStage->id; $noshoots = $score->popns ?? 0; if (!isset($score->dnf) || !$score->dnf) { // Proces times $order = 1; foreach ($score->str as $time) { $strings = null; // parse string splits if (isset($score->meta)) { foreach ($score->meta as $meta) { if (strstr($meta->k, "string" . ($order - 1))) { $strings = array(); $times = explode(",", $meta->v); $curr = 0.0; foreach ($times as $t) { $strings[] = number_format((double)$t - $curr, 2, '.', ''); $curr = (double)$t; } } } } $shooterStageTime = new ShooterStageTime([ 'shooter_stage_id' => $stageNo, 'time' => $time, 'order' => $order++, 'string' => $strings != null ? implode(",", $strings) : null ]); $shooterStageTime->save(); } $dataTpl = array( "shooter_stage_id" => $stageNo, "alpha" => 0, "charlie" => 0, "delta" => 0, "miss" => 0, "popper" => 0, "misspopper" => 0 ); $order = 1; $data = $this->arrayCopy($dataTpl); if ($score->popm > 0 || $score->poph > 0) { $data["order"] = $order++; $data["popper"] = $score->poph; $data["misspopper"] = $score->popm; $target = new ShooterStageTarget($data); $target->save(); $data = $this->arrayCopy($dataTpl); } if (isset($score->ts)) { foreach ($score->ts as $t) { $data["order"] = $order++; $data["alpha"] = ($t & 0xF); $data["charlie"] = ($t & 0xF00) >> 8; $data["delta"] = ($t & 0xF000) >> 12; $data["miss"] = ($t & 0xF00000) >> 20; $noshoots += ($t & 0xF0000) >> 16; $target = new ShooterStageTarget($data); $target->save(); $data = $this->arrayCopy($dataTpl); } } } else { $shooterStageTime = new ShooterStageTime([ 'shooter_stage_id' => $stageNo, 'time' => 9999, 'order' => 1, 'string' => null ]); $shooterStageTime->save(); $target = new ShooterStageTarget(array( "shooter_stage_id" => $stageNo, "alpha" => 0, "charlie" => 0, "delta" => 0, "miss" => 0, "popper" => 0, "misspopper" => 0, "order" => 1 )); $target->save(); } $shooterStage->noshoot = $noshoots; $shooterStage->save(); } } DB::commit(); } private function getUserByAlias(string $alias) { return User::where(DB::raw('UPPER(username)'), strtoupper($alias))->first(); } private function getUserByEmail(string $email) { return User::where(DB::raw('UPPER(email)'), strtoupper($email))->first(); } /** * @param mixed $matchDef * @param Contest $contest * @return void */ public function psProcessShooters(mixed $matchDef, Contest $contest): ?array { $failed = false; $shooters = array(); foreach ($matchDef->match_shooters as $match_shooter) { if (isset($match_shooter->sh_del) && $match_shooter->sh_del) { continue; } $shooter = new stdClass(); $shooter->id = null; $shooter->dq = (isset($match_shooter->sh_dq) && $match_shooter->sh_dq) ? $match_shooter->sh_dq : false; $shooter->firstName = $match_shooter->sh_fn; $shooter->lastName = $match_shooter->sh_ln; $shooter->mail = (isset($match_shooter->sh_eml) && $match_shooter->sh_eml) ? $match_shooter->sh_eml : false; $shooter->uid = $match_shooter->sh_uid; $shooter->division = strtoupper($match_shooter->sh_dvp); $shooter->squad = $match_shooter->sh_sqd; $shooter->alias = (isset($match_shooter->sh_id) && $match_shooter->sh_id) ? strtoupper($match_shooter->sh_id) : false; $shooter->notCompeting = isset($match_shooter->sh_chkins) && in_array("MZ", $match_shooter->sh_chkins); $registration = $contest->registrations()->where('canceltoken', $shooter->uid)->first(); if (!$registration) { // try to create registration $user = $this->getOrCreateUserByAliasOrEmail($shooter->alias, $shooter->mail, $shooter->firstName, $shooter->lastName); if($user == null) { $failed = true; continue; } $registration = new Registration([ 'contest_id' => $contest->id, 'user_id' => $user->id, 'contest_division_id' => 1, 'squad' => $shooter->squad, 'notcomp' => $shooter->notCompeting ? 1 : 0, 'dq' => $shooter->dq ]); $registration->canceltoken = $shooter->uid; $registration->save(); } if (strtoupper($registration->division->bgdivision) != $shooter->division || $shooter->dq || $registration->notcomp != $shooter->notCompeting) { // update division $registration->contest_division_id = ContestDivision::where(DB::raw('UPPER(bgdivision)'), $shooter->division)->first()->id; // DQ $registration->dq = $shooter->dq; // NotCompeting $registration->notcomp = $shooter->notCompeting; $registration->save(); } $shooter->id = $registration->id; if (!$shooter->dq) { $shooters[$shooter->uid] = $shooter; } } return !$failed ? $shooters : null; } public function hasMessages() { return count($this->errors) > 0 || count($this->warnings) > 0; } private function arrayCopy($arr) { $newArray = array(); foreach($arr as $key => $value) { if (is_array($value)) { $newArray[$key] = $this->arrayCopy($value); } elseif (is_object($value)) { $newArray[$key] = clone $value; } else { $newArray[$key] = $value; } } return $newArray; } /** * @param stdClass $shooter * @return User|null */ public function getOrCreateUserByAliasOrEmail($alias, $email, $firstName, $lastName): ?User { if (!$alias && !$email) { $this->errors[] = __('User :firstname :lastname does not have LOS alias or email', ['firstname' => $firstName, 'lastname' => $lastName]); return null; } if (!$alias) { $alias = (string)Uuid::uuid4(); } if (!$email) { $email = Uuid::uuid4() . "@loslex.cz"; } $user = $this->getUserByAlias($alias) ?? $this->getUserByEmail($email); if ($user) { $userLastNameWithSuffix = $user->lastname . ($user->suffix ? " " . $user->suffix : ""); if (!str_starts_with(strtoupper(trim($firstName)), strtoupper($user->firstname)) || !str_starts_with(strtoupper(trim($lastName)), strtoupper($userLastNameWithSuffix))) { $this->errors[] = __('LOS Alias conflict :alias, System: :origfirst :origlast, PractiScore: :psFirst :psLast', ['alias' => $alias, 'origfirst' => $user->firstname, 'origlast' => $user->lastname, 'psFirst' => $firstName, 'psLast' => $lastName]); return null; } } else { // Create new user $this->warnings[] = __('User with name :name and alias :alias does not exist, creating new one.', ['name' => $firstName . " " . $lastName, 'alias' => $alias]); $userdata = [ 'username' => $alias, 'firstname' => $firstName, 'lastname' => $lastName, 'email' => $email, 'password' => "" ]; $validator = Validator::make(array_map("strtolower", $userdata), ['username' => ['required', 'string', 'max:255', 'alpha_num', 'ascii', 'unique:'.User::class, new ForbiddenUsernames ]]); if ($validator->fails()) { $this->warnings = array_merge($this->warnings, $validator->errors()->get('username')); $userdata['username'] = (string)Uuid::uuid4(); } $user = new User($userdata); $user->personaltoken = Str::orderedUuid(); $user->save(); } return $user; } private array $textDivisions = [ 'PISTOLE' => 'PI', 'KOMPAKTNÍ PISTOLE' => 'KPI', 'REVOLVER' => 'RE', 'MALÁ PISTOLE' => 'MPI', 'MALÝ REVOLVER' => 'MRE', 'OPTIK' => 'OPT', 'PUŠKA SAMONABÍJECÍ' => 'PSA', 'OSOBNÍ OBRANNÁ ZBRAŇ' => 'PDW', 'PUŠKA OPAKOVACÍ' => 'POP', 'BROKOVNICE SAMONABÍJECÍ' => 'BSA', 'BROKOVNICE OPAKOVACÍ' => 'BOP', 'BROKOVNICE OSTATNÍ' => 'BOS', ]; }