%PDF- %PDF-
| Direktori : /www/loslex_o/demo/app/Workers/ |
| Current File : /www/loslex_o/demo/app/Workers/ContestImporter.php |
<?php
namespace App\Workers;
use stdClass;
use ZipArchive;
use function Psy\sh;
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\Str;
use Illuminate\Validation\Rule;
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 PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use Ramsey\Uuid\Uuid;
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();
}
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()
{
// Remove cup cache
Cache::forget(CupResultsController::$cacheKey);
Log::info("Cache clear", ['key' => CupResultsController::$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',
];
}