Initial commit
This commit is contained in:
32
app/Models/Band.php
Normal file
32
app/Models/Band.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Band extends Model
|
||||
{
|
||||
protected $table = 'bands';
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'order',
|
||||
'edi_band_begin',
|
||||
'edi_band_end',
|
||||
'has_power_category'
|
||||
];
|
||||
|
||||
public function ediBands(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(EdiBand::class, 'bands_edi_bands', 'band_id', 'edi_band_id');
|
||||
}
|
||||
|
||||
public function contests(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Contest::class, 'contests_bands', 'band_id', 'contest_id');
|
||||
}
|
||||
}
|
||||
29
app/Models/Category.php
Normal file
29
app/Models/Category.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
protected $table = 'categories';
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'order'
|
||||
];
|
||||
|
||||
public function ediCategories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(EdiCategory::class, 'categories_edi_categories', 'category_id', 'edi_category_id');
|
||||
}
|
||||
|
||||
public function contests(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Contest::class, 'contests_categories', 'category_id', 'category_id');
|
||||
}
|
||||
}
|
||||
86
app/Models/Contest.php
Normal file
86
app/Models/Contest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use App\Models\EvaluationRuleSet;
|
||||
|
||||
class Contest extends Model
|
||||
{
|
||||
protected $table = 'contests';
|
||||
|
||||
use HasFactory;
|
||||
use HasTranslations;
|
||||
|
||||
public array $translatable = [
|
||||
'name',
|
||||
'description'
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'url',
|
||||
'evaluator',
|
||||
'email',
|
||||
'email2',
|
||||
'is_mcr',
|
||||
'is_test',
|
||||
'is_sixhr',
|
||||
'is_active',
|
||||
'start_time',
|
||||
'duration',
|
||||
'logs_deadline_days',
|
||||
'rule_set_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'name' => 'array',
|
||||
'description' => 'array',
|
||||
'is_mcr' => 'boolean',
|
||||
'is_test' => 'boolean',
|
||||
'is_sixhr' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'duration' => 'integer',
|
||||
'logs_deadline_days' => 'integer',
|
||||
'rule_set_id' => 'integer',
|
||||
// 'start_time' => 'string', // pokud chceš čistý string; pro fancy práci s časem můžeš dát vlastní cast
|
||||
];
|
||||
|
||||
public function ruleSet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id');
|
||||
}
|
||||
|
||||
public function rounds(): HasMany
|
||||
{
|
||||
return $this->hasMany(Round::class, 'contest_id')
|
||||
->orderByDesc('start_time')
|
||||
->orderByDesc('end_time');
|
||||
}
|
||||
|
||||
public function parameters(): HasMany
|
||||
{
|
||||
return $this->hasMany(ContestParameter::class, 'contest_id');
|
||||
}
|
||||
|
||||
public function bands(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Band::class, 'contests_bands', 'contest_id', 'band_id');
|
||||
}
|
||||
|
||||
public function categories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Category::class, 'contests_categories', 'contest_id', 'category_id');
|
||||
}
|
||||
|
||||
public function powerCategories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(PowerCategory::class, 'contests_power_categories', 'contest_id', 'power_category_id');
|
||||
}
|
||||
}
|
||||
50
app/Models/ContestParameter.php
Normal file
50
app/Models/ContestParameter.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ContestParameter extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'contests_parameters';
|
||||
|
||||
protected $fillable = [
|
||||
'contest_id',
|
||||
'log_type',
|
||||
'ignore_slash_part',
|
||||
'ignore_third_part',
|
||||
'letters_in_rst',
|
||||
'discard_qso_rec_diff_call',
|
||||
'discard_qso_sent_diff_call',
|
||||
'discard_qso_rec_diff_rst',
|
||||
'discard_qso_sent_diff_rst',
|
||||
'discard_qso_rec_diff_code',
|
||||
'discard_qso_sent_diff_code',
|
||||
'unique_qso',
|
||||
'time_tolerance',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'contest_id' => 'integer',
|
||||
'ignore_slash_part' => 'boolean',
|
||||
'ignore_third_part' => 'boolean',
|
||||
'letters_in_rst' => 'boolean',
|
||||
'discard_qso_rec_diff_call' => 'boolean',
|
||||
'discard_qso_sent_diff_call' => 'boolean',
|
||||
'discard_qso_rec_diff_rst' => 'boolean',
|
||||
'discard_qso_sent_diff_rst' => 'boolean',
|
||||
'discard_qso_rec_diff_code' => 'boolean',
|
||||
'discard_qso_sent_diff_code' => 'boolean',
|
||||
'unique_qso' => 'boolean',
|
||||
'time_tolerance' => 'integer',
|
||||
];
|
||||
|
||||
public function contest(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Contest::class, 'contest_id');
|
||||
}
|
||||
}
|
||||
31
app/Models/CountryWwl.php
Normal file
31
app/Models/CountryWwl.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CountryWwl extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'countries_wwl';
|
||||
|
||||
// Nemáme primární klíč typu int → vypnout auto increment
|
||||
public $incrementing = false;
|
||||
|
||||
// Primární klíč není integer → type = string
|
||||
protected $keyType = 'string';
|
||||
|
||||
// Eloquent NEPODPORUJE composite PK → necháme primaryKey prázdné
|
||||
protected $primaryKey = null;
|
||||
|
||||
// Zakázat timestamps auto-handling? → ne, používáš timestamps v DB
|
||||
// Pokud bys je chtěl řídit sám:
|
||||
// public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'country_name',
|
||||
'wwl'
|
||||
];
|
||||
}
|
||||
38
app/Models/Cty.php
Normal file
38
app/Models/Cty.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class Cty extends Model
|
||||
{
|
||||
protected $table = 'cty';
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'country_name',
|
||||
'dxcc',
|
||||
'cq_zone',
|
||||
'itu_zone',
|
||||
'continent',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'time_offset',
|
||||
'prefix',
|
||||
'prefix_norm',
|
||||
'precise',
|
||||
'source'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'dxcc' => 'integer',
|
||||
'cq_zone' => 'integer',
|
||||
'itu_zone' => 'integer',
|
||||
'latitude' => 'float',
|
||||
'longitude' => 'float',
|
||||
'time_offset' => 'float',
|
||||
'precise' => 'boolean',
|
||||
];
|
||||
}
|
||||
23
app/Models/EdiBand.php
Normal file
23
app/Models/EdiBand.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class EdiBand extends Model
|
||||
{
|
||||
protected $table = 'edi_bands';
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'value'
|
||||
];
|
||||
|
||||
public function bands(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Band::class, 'bands_edi_bands', 'edi_band_id', 'band_id');
|
||||
}
|
||||
}
|
||||
24
app/Models/EdiCategory.php
Normal file
24
app/Models/EdiCategory.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class EdiCategory extends Model
|
||||
{
|
||||
protected $table = 'edi_categories';
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'value',
|
||||
'regex_pattern',
|
||||
];
|
||||
|
||||
public function categories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Category::class, 'categories_edi_categories', 'edi_category_id', 'category_id');
|
||||
}
|
||||
}
|
||||
148
app/Models/EvaluationLock.php
Normal file
148
app/Models/EvaluationLock.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* Model: EvaluationLock
|
||||
*
|
||||
* Účel:
|
||||
* - Reprezentuje aplikační zámek (lock) pro vyhodnocovací procesy.
|
||||
* - Slouží k zabránění souběžného spuštění více vyhodnocovacích běhů
|
||||
* nad stejným rozsahem dat (scope).
|
||||
*
|
||||
* Kontext v architektuře:
|
||||
* - Používá se při spuštění vyhodnocení (StartEvaluationRunJob)
|
||||
* a během celé evaluation pipeline.
|
||||
* - Je součástí mechanismu ochrany proti race condition a kolizím
|
||||
* mezi paralelně běžícími background joby.
|
||||
*
|
||||
* Princip fungování:
|
||||
* - Každý lock je identifikován unikátním klíčem (`key`), který
|
||||
* reprezentuje zamčený rozsah dat, typicky např.:
|
||||
* - `round:{round_id}`
|
||||
* - `round:{round_id}:band:{band_id}`
|
||||
* - Při pokusu o vytvoření locku se spoléhá na unikátní index v DB.
|
||||
* Pokud záznam s daným klíčem již existuje, znamená to, že jiný
|
||||
* vyhodnocovací běh nad stejným scope již probíhá.
|
||||
*
|
||||
* Pole modelu:
|
||||
* - key:
|
||||
* Jednoznačný identifikátor zamčeného scope (unikátní v DB).
|
||||
* - evaluation_run_id:
|
||||
* Reference na EvaluationRun, který lock vlastní.
|
||||
* - locked_at:
|
||||
* Čas, kdy byl lock získán.
|
||||
* - expires_at:
|
||||
* Volitelný čas expirace locku (slouží jako bezpečnostní pojistka
|
||||
* proti "visícím" lockům při havárii procesu).
|
||||
*
|
||||
* Co model NEDĚLÁ:
|
||||
* - neřeší automatické obnovování nebo expirování locků
|
||||
* - neobsahuje byznys logiku vyhodnocení
|
||||
* - nesupluje databázové transakce
|
||||
*
|
||||
* Zásady návrhu:
|
||||
* - Lock musí být získáván a uvolňován atomicky.
|
||||
* - Locky musí být vždy uvolněny ve finálním kroku vyhodnocení
|
||||
* (FinalizeRunJob), případně při chybě.
|
||||
* - Existence locku je autoritativní zdroj informace o tom,
|
||||
* zda je daný scope právě zpracováván.
|
||||
*/
|
||||
class EvaluationLock extends Model
|
||||
{
|
||||
protected $table = 'evaluation_locks';
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'evaluation_run_id',
|
||||
'locked_at',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'evaluation_run_id' => 'integer',
|
||||
'locked_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function evaluationRun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRun::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pokusí se získat lock pro daný klíč.
|
||||
*
|
||||
* - Pokud existuje expirovaný lock, před pokusem ho smaže.
|
||||
* - Pokud už aktivní lock existuje, vrátí null.
|
||||
*
|
||||
* @param string $key Jedinečný identifikátor scope (např. "round:5").
|
||||
* @param \App\Models\EvaluationRun|null $run Volitelný běh, ke kterému se lock váže.
|
||||
* @param \DateInterval|int|\Carbon\Carbon|null $ttl Doba platnosti (sekundy nebo DateInterval nebo konkrétní expirace).
|
||||
*/
|
||||
public static function acquire(string $key, ?EvaluationRun $run = null, \DateInterval|int|Carbon|null $ttl = null): ?self
|
||||
{
|
||||
$now = Carbon::now();
|
||||
|
||||
// uklidíme expirovaný lock se stejným klíčem, aby neblokoval unikátní index
|
||||
self::where('key', $key)
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<', $now)
|
||||
->delete();
|
||||
|
||||
$expiresAt = null;
|
||||
if ($ttl instanceof Carbon) {
|
||||
$expiresAt = $ttl;
|
||||
} elseif ($ttl instanceof \DateInterval) {
|
||||
$expiresAt = (clone $now)->add($ttl);
|
||||
} elseif (is_int($ttl)) {
|
||||
$expiresAt = (clone $now)->addSeconds($ttl);
|
||||
}
|
||||
|
||||
try {
|
||||
return self::create([
|
||||
'key' => $key,
|
||||
'evaluation_run_id' => $run?->id,
|
||||
'locked_at' => $now,
|
||||
'expires_at' => $expiresAt,
|
||||
]);
|
||||
} catch (QueryException $e) {
|
||||
// unikátní klíč porušen -> lock už drží někdo jiný
|
||||
if (str_contains(strtolower($e->getMessage()), 'duplicate') || str_contains(strtolower($e->getMessage()), 'unique')) {
|
||||
return null;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uvolní lock podle klíče (a volitelně evaluation_run_id).
|
||||
*/
|
||||
public static function release(string $key, ?EvaluationRun $run = null): int
|
||||
{
|
||||
$query = self::where('key', $key);
|
||||
if ($run) {
|
||||
$query->where('evaluation_run_id', $run->id);
|
||||
}
|
||||
return $query->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Zjistí, zda lock existuje a neexpiroval.
|
||||
*/
|
||||
public static function isLocked(string $key): bool
|
||||
{
|
||||
$now = Carbon::now();
|
||||
return self::where('key', $key)
|
||||
->where(function ($q) use ($now) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', $now);
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
479
app/Models/EvaluationRuleSet.php
Normal file
479
app/Models/EvaluationRuleSet.php
Normal file
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class EvaluationRuleSet extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'evaluation_rule_sets';
|
||||
|
||||
/**
|
||||
* Atributy rulesetu používané při vyhodnocování.
|
||||
*
|
||||
* Skupiny:
|
||||
* - scoring_mode/points_per_*: způsob bodování.
|
||||
* - multipliers*: multiplikátory a jejich scope.
|
||||
* - *_policy / penalty_*: penalizace a způsob započtení chyb.
|
||||
* - exchange_* / discard_*: pravidla výměny a busted detekce.
|
||||
* - match_* / callsign_*: pravidla párování a normalizace.
|
||||
* - out_of_window_*: chování mimo časové okno.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'code',
|
||||
'description',
|
||||
|
||||
|
||||
'scoring_mode', // DISTANCE / FIXED_POINTS
|
||||
'points_per_qso', // Fixní body za valid QSO
|
||||
'points_per_km', // Body za km v režimu DISTANCE
|
||||
|
||||
'use_multipliers', // Zapnout multiplikátory ve výsledku
|
||||
'multiplier_type', // NONE / WWL / DXCC / SECTION / COUNTRY
|
||||
|
||||
'dup_qso_policy', // ZERO_POINTS / PENALTY
|
||||
'nil_qso_policy', // ZERO_POINTS / PENALTY
|
||||
'no_counterpart_log_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
|
||||
'not_in_counterpart_log_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
|
||||
'unique_qso_policy', // INVALID / ZERO_POINTS / FLAG_ONLY
|
||||
'busted_call_policy', // ZERO_POINTS / PENALTY
|
||||
'busted_exchange_policy',// ZERO_POINTS / PENALTY
|
||||
'busted_serial_policy', // ZERO_POINTS / PENALTY
|
||||
'busted_locator_policy', // ZERO_POINTS / PENALTY
|
||||
'penalty_dup_points', // Penalizace za DUP
|
||||
'penalty_nil_points', // Penalizace za NIL
|
||||
'penalty_busted_call_points', // Penalizace za BUSTED_CALL
|
||||
'penalty_busted_exchange_points', // Penalizace za BUSTED_EXCHANGE
|
||||
'penalty_busted_serial_points', // Penalizace za BUSTED_SERIAL
|
||||
'penalty_busted_locator_points', // Penalizace za BUSTED_LOCATOR
|
||||
'dupe_scope', // BAND / BAND_MODE
|
||||
'callsign_normalization',// STRICT / IGNORE_SUFFIX
|
||||
'distance_rounding', // FLOOR / ROUND / CEIL
|
||||
'min_distance_km', // Minimum km pro bodované QSO
|
||||
'require_locators', // Bez lokátorů = nevalidní / bez bodů
|
||||
'out_of_window_policy', // IGNORE / ZERO_POINTS / PENALTY / INVALID
|
||||
'penalty_out_of_window_points', // Penalizace za OUT_OF_WINDOW
|
||||
|
||||
'exchange_type', // SERIAL / WWL / SERIAL_WWL / CUSTOM
|
||||
'exchange_requires_wwl', // WWL je povinná část výměny
|
||||
'exchange_requires_serial', // Serial je povinná část výměny
|
||||
'exchange_requires_report', // RST je povinná část výměny
|
||||
'exchange_pattern', // Regex pro CUSTOM výměnu
|
||||
|
||||
'ignore_slash_part', // Ignorovat suffix za lomítkem v callsign
|
||||
'ignore_third_part', // Ignorovat 3. část callsign
|
||||
'letters_in_rst', // Povolit písmena v RST
|
||||
'rst_ignore_third_char', // Ignorovat 3. znak v RST
|
||||
'discard_qso_rec_diff_call', // RX neshoda callsign -> BUSTED_CALL (error_side=RX)
|
||||
'discard_qso_sent_diff_call', // TX neshoda callsign -> BUSTED_CALL (error_side=TX)
|
||||
'discard_qso_rec_diff_rst', // RX neshoda RST -> BUSTED_RST (error_side=RX)
|
||||
'discard_qso_sent_diff_rst', // TX neshoda RST -> BUSTED_RST (error_side=TX)
|
||||
'discard_qso_rec_diff_code', // RX neshoda exchange -> BUSTED_SERIAL/LOCATOR (error_side=RX)
|
||||
'discard_qso_sent_diff_code',// TX neshoda exchange -> BUSTED_SERIAL/LOCATOR (error_side=TX)
|
||||
'discard_qso_rec_diff_serial', // RX neshoda serial -> BUSTED_SERIAL (error_side=RX)
|
||||
'discard_qso_sent_diff_serial',// TX neshoda serial -> BUSTED_SERIAL (error_side=TX)
|
||||
'discard_qso_rec_diff_wwl', // RX neshoda WWL -> BUSTED_LOCATOR (error_side=RX)
|
||||
'discard_qso_sent_diff_wwl', // TX neshoda WWL -> BUSTED_LOCATOR (error_side=TX)
|
||||
'busted_rst_policy', // ZERO_POINTS / PENALTY
|
||||
'penalty_busted_rst_points', // Penalizace za BUSTED_RST
|
||||
|
||||
'match_tiebreak_order', // Pořadí tiebreak kritérií
|
||||
'match_require_locator_match', // Matching vyžaduje lokátor
|
||||
'match_require_exchange_match',// Matching vyžaduje exchange
|
||||
|
||||
'multiplier_scope', // PER_BAND / OVERALL
|
||||
'multiplier_source', // VALID_ONLY / ALL_MATCHED
|
||||
'wwl_multiplier_level', // LOCATOR_2 / LOCATOR_4 / LOCATOR_6
|
||||
|
||||
'checklog_matching', // CHECK logy v matchingu
|
||||
'out_of_window_dq_threshold', // DQ logu při nadlimitních OOW QSO
|
||||
'time_diff_dq_threshold_percent', // DQ logu při nadlimitním % časového rozdílu
|
||||
'time_diff_dq_threshold_sec', // Prah časového rozdílu v sekundách
|
||||
'bad_qso_dq_threshold_percent', // DQ logu při nadlimitním % špatných QSO
|
||||
|
||||
'time_tolerance_sec', // Tolerance času v matchingu (sekundy)
|
||||
'require_unique_qso', // Zapnout detekci duplicit v logu
|
||||
'allow_time_shift_one_hour', // Povolit posun o 1 hodinu při matchingu
|
||||
'time_shift_seconds', // Velikost časového posunu v sekundách
|
||||
'time_mismatch_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
|
||||
'callsign_suffix_max_len', // Max délka suffixu za /
|
||||
'callsign_levenshtein_max', // Maximální Levenshtein vzdálenost pro fuzzy match
|
||||
'allow_time_mismatch_pairing', // Povolit párování mimo toleranci
|
||||
'time_mismatch_max_sec', // Max. odchylka mimo toleranci (NULL = bez limitu)
|
||||
'dup_resolution_strategy', // Pořadí pravidel pro volbu survivor v duplicitách
|
||||
|
||||
'operating_window_mode', // NONE / BEST_CONTIGUOUS
|
||||
'operating_window_hours', // Délka okna v hodinách
|
||||
'sixhr_ranking_mode', // IARU / CRK
|
||||
|
||||
'options', // Volitelné JSON rozšíření pravidel
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
'points_per_qso' => 'integer',
|
||||
'points_per_km' => 'float',
|
||||
|
||||
'use_multipliers' => 'boolean',
|
||||
'time_tolerance_sec' => 'integer',
|
||||
'require_unique_qso' => 'boolean',
|
||||
'penalty_dup_points' => 'integer',
|
||||
'penalty_nil_points' => 'integer',
|
||||
'penalty_busted_call_points' => 'integer',
|
||||
'penalty_busted_exchange_points' => 'integer',
|
||||
'penalty_busted_serial_points' => 'integer',
|
||||
'penalty_busted_locator_points' => 'integer',
|
||||
'min_distance_km' => 'integer',
|
||||
'require_locators' => 'boolean',
|
||||
'penalty_out_of_window_points' => 'integer',
|
||||
|
||||
'exchange_requires_wwl' => 'boolean',
|
||||
'exchange_requires_serial' => 'boolean',
|
||||
'exchange_requires_report' => 'boolean',
|
||||
'ignore_slash_part' => 'boolean',
|
||||
'ignore_third_part' => 'boolean',
|
||||
'letters_in_rst' => 'boolean',
|
||||
'rst_ignore_third_char' => 'boolean',
|
||||
'discard_qso_rec_diff_call' => 'boolean',
|
||||
'discard_qso_sent_diff_call' => 'boolean',
|
||||
'discard_qso_rec_diff_rst' => 'boolean',
|
||||
'discard_qso_sent_diff_rst' => 'boolean',
|
||||
'discard_qso_rec_diff_code' => 'boolean',
|
||||
'discard_qso_sent_diff_code' => 'boolean',
|
||||
'discard_qso_rec_diff_serial' => 'boolean',
|
||||
'discard_qso_sent_diff_serial' => 'boolean',
|
||||
'discard_qso_rec_diff_wwl' => 'boolean',
|
||||
'discard_qso_sent_diff_wwl' => 'boolean',
|
||||
'penalty_busted_rst_points' => 'integer',
|
||||
|
||||
'match_tiebreak_order' => 'array',
|
||||
'match_require_locator_match' => 'boolean',
|
||||
'match_require_exchange_match' => 'boolean',
|
||||
|
||||
'checklog_matching' => 'boolean',
|
||||
'out_of_window_dq_threshold' => 'integer',
|
||||
'time_diff_dq_threshold_percent' => 'integer',
|
||||
'time_diff_dq_threshold_sec' => 'integer',
|
||||
'bad_qso_dq_threshold_percent' => 'integer',
|
||||
'allow_time_shift_one_hour' => 'boolean',
|
||||
'time_shift_seconds' => 'integer',
|
||||
'callsign_suffix_max_len' => 'integer',
|
||||
'callsign_levenshtein_max' => 'integer',
|
||||
'allow_time_mismatch_pairing' => 'boolean',
|
||||
'time_mismatch_max_sec' => 'integer',
|
||||
'dup_resolution_strategy' => 'array',
|
||||
|
||||
'operating_window_mode' => 'string',
|
||||
'operating_window_hours' => 'integer',
|
||||
'sixhr_ranking_mode' => 'string',
|
||||
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mapování: kde je jednotlivý atribut rulesetu použit.
|
||||
* Pokud je hodnota "unused", musí být explicitně zdůvodněno.
|
||||
*/
|
||||
public const FLAG_USAGE = [
|
||||
'name' => 'metadata (UI, identifikace rulesetu)',
|
||||
'code' => 'metadata (UI, identifikace rulesetu)',
|
||||
'description' => 'metadata (UI, identifikace rulesetu)',
|
||||
'scoring_mode' => 'ScoringService::computeBasePoints',
|
||||
'points_per_qso' => 'ScoringService::computeBasePoints',
|
||||
'points_per_km' => 'ScoringService::computeBasePoints',
|
||||
'use_multipliers' => 'ScoreGroupJob::applyMultipliers + AggregateLogResultsJob',
|
||||
'multiplier_type' => 'ScoreGroupJob::applyMultipliers + AggregateLogResultsJob',
|
||||
'dup_qso_policy' => 'ScoringService::penaltyPointsFor',
|
||||
'nil_qso_policy' => 'ScoringService::penaltyPointsFor',
|
||||
'no_counterpart_log_policy' => 'ScoreGroupJob (validita/bodování)',
|
||||
'not_in_counterpart_log_policy' => 'ScoreGroupJob (validita/bodování)',
|
||||
'unique_qso_policy' => 'ScoreGroupJob (validita/bodování)',
|
||||
'busted_call_policy' => 'ScoringService::penaltyPointsFor',
|
||||
'busted_exchange_policy' => 'ScoringService::penaltyPointsFor',
|
||||
'busted_serial_policy' => 'ScoringService::penaltyPointsFor',
|
||||
'busted_locator_policy' => 'ScoringService::penaltyPointsFor',
|
||||
'penalty_dup_points' => 'ScoringService::penaltyPointsFor',
|
||||
'penalty_nil_points' => 'ScoringService::penaltyPointsFor',
|
||||
'penalty_busted_call_points' => 'ScoringService::penaltyPointsFor',
|
||||
'penalty_busted_exchange_points' => 'ScoringService::penaltyPointsFor',
|
||||
'penalty_busted_serial_points' => 'ScoringService::penaltyPointsFor',
|
||||
'penalty_busted_locator_points' => 'ScoringService::penaltyPointsFor',
|
||||
'dupe_scope' => 'BuildWorkingSetLogJob::dupe_key',
|
||||
'callsign_normalization' => 'MatchingService::normalizeCallsign + MatchQsoGroupJob',
|
||||
'distance_rounding' => 'ScoringService::calculateDistanceKm',
|
||||
'min_distance_km' => 'ScoringService::calculateDistanceKm',
|
||||
'require_locators' => 'ScoreGroupJob (validita/bodování)',
|
||||
'out_of_window_policy' => 'ScoringService::outOfWindowDecision + ScoreGroupJob',
|
||||
'penalty_out_of_window_points' => 'ScoringService::penaltyPointsFor',
|
||||
'exchange_type' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||||
'exchange_requires_wwl' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||||
'exchange_requires_serial' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||||
'exchange_requires_report' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||||
'exchange_pattern' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||||
'ignore_slash_part' => 'MatchQsoGroupJob::normalizeCallsign',
|
||||
'ignore_third_part' => 'MatchQsoGroupJob::normalizeCallsign',
|
||||
'letters_in_rst' => 'MatchQsoGroupJob::normalizeRst',
|
||||
'rst_ignore_third_char' => 'MatchQsoGroupJob::reportsMatch',
|
||||
'discard_qso_rec_diff_call' => 'MatchQsoGroupJob (error_side=RX)',
|
||||
'discard_qso_sent_diff_call' => 'MatchQsoGroupJob (error_side=TX)',
|
||||
'discard_qso_rec_diff_rst' => 'MatchQsoGroupJob (error_side=RX)',
|
||||
'discard_qso_sent_diff_rst' => 'MatchQsoGroupJob (error_side=TX)',
|
||||
'discard_qso_rec_diff_code' => 'MatchQsoGroupJob (RX exchange mismatch)',
|
||||
'discard_qso_sent_diff_code' => 'MatchQsoGroupJob (TX exchange mismatch)',
|
||||
'discard_qso_rec_diff_serial' => 'MatchQsoGroupJob (RX serial mismatch)',
|
||||
'discard_qso_sent_diff_serial' => 'MatchQsoGroupJob (TX serial mismatch)',
|
||||
'discard_qso_rec_diff_wwl' => 'MatchQsoGroupJob (RX locator mismatch)',
|
||||
'discard_qso_sent_diff_wwl' => 'MatchQsoGroupJob (TX locator mismatch)',
|
||||
'busted_rst_policy' => 'ScoringService::penaltyPointsFor',
|
||||
'penalty_busted_rst_points' => 'ScoringService::penaltyPointsFor',
|
||||
'match_tiebreak_order' => 'MatchQsoGroupJob::rankDecision',
|
||||
'match_require_locator_match' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||||
'match_require_exchange_match' => 'MatchQsoGroupJob::resolveExchangeMatch',
|
||||
'multiplier_scope' => 'AggregateLogResultsJob::aggregateMultipliers',
|
||||
'multiplier_source' => 'AggregateLogResultsJob::aggregateMultipliers',
|
||||
'wwl_multiplier_level' => 'ScoreGroupJob::applyMultipliers',
|
||||
'checklog_matching' => 'MatchQsoGroupJob::groupLogsByKey',
|
||||
'out_of_window_dq_threshold' => 'AggregateLogResultsJob (DQ logu)',
|
||||
'time_diff_dq_threshold_percent' => 'AggregateLogResultsJob (DQ logu)',
|
||||
'time_diff_dq_threshold_sec' => 'AggregateLogResultsJob (DQ logu)',
|
||||
'bad_qso_dq_threshold_percent' => 'AggregateLogResultsJob (DQ logu)',
|
||||
'time_tolerance_sec' => 'MatchQsoGroupJob::findCandidates',
|
||||
'require_unique_qso' => 'UnpairedClassificationJob::UNIQUE',
|
||||
'allow_time_shift_one_hour' => 'MatchQsoGroupJob::matchWithTimeShift',
|
||||
'time_shift_seconds' => 'MatchQsoGroupJob::matchWithTimeShift',
|
||||
'time_mismatch_policy' => 'ScoreGroupJob (validita/bodování)',
|
||||
'callsign_suffix_max_len' => 'MatchingService::normalizeCallsign',
|
||||
'callsign_levenshtein_max' => 'MatchQsoGroupJob::rankDecision',
|
||||
'allow_time_mismatch_pairing' => 'MatchQsoGroupJob::findCandidates',
|
||||
'time_mismatch_max_sec' => 'MatchQsoGroupJob::findCandidates',
|
||||
'dup_resolution_strategy' => 'DuplicateResolutionJob::sort',
|
||||
'operating_window_mode' => 'AggregateLogResultsJob (operating window, 6H agregace)',
|
||||
'operating_window_hours' => 'AggregateLogResultsJob (operating window, 6H agregace)',
|
||||
'sixhr_ranking_mode' => 'RecalculateOfficialRanksJob (6H ranking mode)',
|
||||
'options' => 'fallback pro hodnoty bez sloupců (EvaluationRuleSet::getOption)',
|
||||
];
|
||||
|
||||
/**
|
||||
* Options fallback (používá se jen pokud není vyplněn odpovídající sloupec).
|
||||
*
|
||||
* Klíče, typy, defaulty a fáze použití:
|
||||
* - ignore_slash_part (bool, default=true) – matching (normalizace callsign)
|
||||
* - callsign_suffix_max_len (int, default=4) – matching (normalizace callsign)
|
||||
* - rst_ignore_third_char (bool, default=true) – matching (porovnání RST)
|
||||
* - letters_in_rst (bool, default=false) – matching (porovnání RST)
|
||||
* - callsign_levenshtein_max (int, default=0) – matching (fuzzy callsign)
|
||||
* - allow_time_shift_one_hour (bool, default=true) – matching (časový posun)
|
||||
* - time_shift_seconds (int, default=3600) – matching (časový posun)
|
||||
* - allow_time_mismatch_pairing (bool, default=false) – matching (TIME_MISMATCH pairing)
|
||||
* - time_mismatch_max_sec (int|null, default=null) – matching (max odchylka mimo toleranci)
|
||||
* - match_require_locator_match (bool, default=false) – matching (mismatch flagging)
|
||||
* - match_require_exchange_match (bool, default=false) – matching (mismatch flagging)
|
||||
* - unique_qso_enabled (bool, default=true) – unpaired klasifikace (UNIQUE)
|
||||
* - unique_qso_policy (string, default=FLAG_ONLY) – scoring (validita/bodování)
|
||||
* - no_counterpart_log_policy (string, default=FLAG_ONLY) – scoring
|
||||
* - not_in_counterpart_log_policy (string, default=ZERO_POINTS) – scoring
|
||||
* - duplicate_resolution_strategy (array, default=[paired_first, ok_first, earlier_time, lower_id]) – duplicity
|
||||
* - distance_rounding (string, default=CEIL) – scoring (vzdálenost)
|
||||
* - min_distance_km (int, default=1) – scoring (vzdálenost)
|
||||
*/
|
||||
|
||||
// ----- Vztahy -----
|
||||
|
||||
public function evaluationRuns(): HasMany
|
||||
{
|
||||
return $this->hasMany(EvaluationRun::class, 'rule_set_id');
|
||||
}
|
||||
|
||||
// ----- Pomocné metody pro logiku vyhodnocení -----
|
||||
|
||||
public function isDistanceScoring(): bool
|
||||
{
|
||||
return $this->scoring_mode === 'DISTANCE';
|
||||
}
|
||||
|
||||
public function isFixedPointsScoring(): bool
|
||||
{
|
||||
return $this->scoring_mode === 'FIXED_POINTS';
|
||||
}
|
||||
|
||||
public function usesMultipliers(): bool
|
||||
{
|
||||
return $this->use_multipliers && $this->multiplier_type !== 'NONE';
|
||||
}
|
||||
|
||||
public function multiplierIsWwl(): bool
|
||||
{
|
||||
return $this->multiplier_type === 'WWL';
|
||||
}
|
||||
|
||||
public function multiplierIsDxcc(): bool
|
||||
{
|
||||
return $this->multiplier_type === 'DXCC';
|
||||
}
|
||||
|
||||
public function multiplierIsSection(): bool
|
||||
{
|
||||
return $this->multiplier_type === 'SECTION';
|
||||
}
|
||||
|
||||
public function multiplierIsCountry(): bool
|
||||
{
|
||||
return $this->multiplier_type === 'COUNTRY';
|
||||
}
|
||||
|
||||
public function dupCountsAsPenalty(): bool
|
||||
{
|
||||
return $this->dup_qso_policy === 'PENALTY';
|
||||
}
|
||||
|
||||
// ----- Options fallback & typed accessors -----
|
||||
|
||||
protected function getOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$options = $this->options ?? [];
|
||||
return array_key_exists($key, $options) ? $options[$key] : $default;
|
||||
}
|
||||
|
||||
public function getString(string $key, ?string $columnValue, ?string $default = null): ?string
|
||||
{
|
||||
if ($columnValue !== null) {
|
||||
return $columnValue;
|
||||
}
|
||||
$value = $this->getOption($key, $default);
|
||||
return is_string($value) ? $value : $default;
|
||||
}
|
||||
|
||||
public function getBool(string $key, ?bool $columnValue, bool $default = false): bool
|
||||
{
|
||||
if ($columnValue !== null) {
|
||||
return (bool) $columnValue;
|
||||
}
|
||||
return (bool) $this->getOption($key, $default);
|
||||
}
|
||||
|
||||
public function getInt(string $key, ?int $columnValue, ?int $default = null): ?int
|
||||
{
|
||||
if ($columnValue !== null) {
|
||||
return (int) $columnValue;
|
||||
}
|
||||
$value = $this->getOption($key, $default);
|
||||
return $value === null ? null : (int) $value;
|
||||
}
|
||||
|
||||
public function getArray(string $key, ?array $columnValue, array $default = []): array
|
||||
{
|
||||
if ($columnValue !== null) {
|
||||
return $columnValue;
|
||||
}
|
||||
$value = $this->getOption($key, $default);
|
||||
return is_array($value) ? $value : $default;
|
||||
}
|
||||
|
||||
public function ignoreSlashPart(): bool
|
||||
{
|
||||
return $this->getBool('ignore_slash_part', $this->ignore_slash_part);
|
||||
}
|
||||
|
||||
public function ignoreThirdPart(): bool
|
||||
{
|
||||
return $this->getBool('ignore_third_part', $this->ignore_third_part);
|
||||
}
|
||||
|
||||
public function lettersInRst(): bool
|
||||
{
|
||||
return $this->getBool('letters_in_rst', $this->letters_in_rst);
|
||||
}
|
||||
|
||||
public function callsignLevenshteinMax(): int
|
||||
{
|
||||
return (int) ($this->getInt('callsign_levenshtein_max', $this->callsign_levenshtein_max, 0) ?? 0);
|
||||
}
|
||||
|
||||
public function callsignSuffixMaxLen(): int
|
||||
{
|
||||
return (int) ($this->getInt('callsign_suffix_max_len', $this->callsign_suffix_max_len, 4) ?? 4);
|
||||
}
|
||||
|
||||
public function rstIgnoreThirdChar(): bool
|
||||
{
|
||||
return $this->getBool('rst_ignore_third_char', $this->rst_ignore_third_char, true);
|
||||
}
|
||||
|
||||
public function matchRequireLocatorMatch(): bool
|
||||
{
|
||||
return $this->getBool('match_require_locator_match', $this->match_require_locator_match, false);
|
||||
}
|
||||
|
||||
public function matchRequireExchangeMatch(): bool
|
||||
{
|
||||
return $this->getBool('match_require_exchange_match', $this->match_require_exchange_match, false);
|
||||
}
|
||||
|
||||
public function allowTimeShiftOneHour(): bool
|
||||
{
|
||||
return $this->getBool('allow_time_shift_one_hour', $this->allow_time_shift_one_hour, true);
|
||||
}
|
||||
|
||||
public function allowTimeMismatchPairing(): bool
|
||||
{
|
||||
return $this->getBool('allow_time_mismatch_pairing', $this->allow_time_mismatch_pairing, false);
|
||||
}
|
||||
|
||||
public function timeMismatchMaxSec(): ?int
|
||||
{
|
||||
return $this->getInt('time_mismatch_max_sec', $this->time_mismatch_max_sec, null);
|
||||
}
|
||||
|
||||
public function uniqueQsoEnabled(): bool
|
||||
{
|
||||
return $this->getBool('unique_qso_enabled', $this->require_unique_qso, true);
|
||||
}
|
||||
|
||||
public function dupResolutionStrategy(): array
|
||||
{
|
||||
$fallback = $this->getArray('duplicate_resolution_strategy', null, [
|
||||
'paired_first',
|
||||
'ok_first',
|
||||
'earlier_time',
|
||||
'lower_id',
|
||||
]);
|
||||
|
||||
$strategy = $this->getArray('duplicate_resolution_strategy', $this->dup_resolution_strategy, $fallback);
|
||||
if ($strategy !== $fallback) {
|
||||
return $strategy;
|
||||
}
|
||||
|
||||
return $this->getArray('dup_resolution_strategy', $this->dup_resolution_strategy, $fallback);
|
||||
}
|
||||
|
||||
public function distanceRounding(): string
|
||||
{
|
||||
return $this->getString('distance_rounding', $this->distance_rounding, 'CEIL') ?? 'CEIL';
|
||||
}
|
||||
|
||||
public function minDistanceKm(): ?int
|
||||
{
|
||||
return $this->getInt('min_distance_km', $this->min_distance_km, 1);
|
||||
}
|
||||
|
||||
public function nilCountsAsPenalty(): bool
|
||||
{
|
||||
return $this->nil_qso_policy === 'PENALTY';
|
||||
}
|
||||
|
||||
public function bustedCallCountsAsPenalty(): bool
|
||||
{
|
||||
return $this->busted_call_policy === 'PENALTY';
|
||||
}
|
||||
|
||||
public function bustedExchangeCountsAsPenalty(): bool
|
||||
{
|
||||
return $this->busted_exchange_policy === 'PENALTY';
|
||||
}
|
||||
|
||||
}
|
||||
95
app/Models/EvaluationRun.php
Normal file
95
app/Models/EvaluationRun.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use App\Models\User;
|
||||
use App\Models\EvaluationLock;
|
||||
use App\Models\EvaluationRunEvent;
|
||||
|
||||
class EvaluationRun extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'evaluation_runs';
|
||||
|
||||
protected $fillable = [
|
||||
'round_id',
|
||||
'rule_set_id',
|
||||
'name',
|
||||
'rules_version',
|
||||
'result_type',
|
||||
'is_official',
|
||||
'notes',
|
||||
'status',
|
||||
'batch_id',
|
||||
'current_step',
|
||||
'progress_total',
|
||||
'progress_done',
|
||||
'scope',
|
||||
'error',
|
||||
'started_at',
|
||||
'finished_at',
|
||||
'created_by_user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'round_id' => 'integer',
|
||||
'rule_set_id' => 'integer',
|
||||
'is_official' => 'boolean',
|
||||
'result_type' => 'string',
|
||||
'progress_total' => 'integer',
|
||||
'progress_done' => 'integer',
|
||||
'scope' => 'array',
|
||||
'started_at' => 'datetime',
|
||||
'finished_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function round(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Round::class);
|
||||
}
|
||||
|
||||
public function logResults(): HasMany
|
||||
{
|
||||
return $this->hasMany(LogResult::class);
|
||||
}
|
||||
|
||||
public function qsoResults(): HasMany
|
||||
{
|
||||
return $this->hasMany(QsoResult::class);
|
||||
}
|
||||
|
||||
public function evaluationLocks(): HasMany
|
||||
{
|
||||
return $this->hasMany(EvaluationLock::class);
|
||||
}
|
||||
|
||||
public function events(): HasMany
|
||||
{
|
||||
return $this->hasMany(EvaluationRunEvent::class);
|
||||
}
|
||||
|
||||
public function ruleSet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id');
|
||||
}
|
||||
|
||||
public function createdBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||
}
|
||||
|
||||
public function isCanceled(): bool
|
||||
{
|
||||
return strtoupper((string) $this->status) === 'CANCELED';
|
||||
}
|
||||
|
||||
public static function isCanceledRun(int $runId): bool
|
||||
{
|
||||
return static::where('id', $runId)->value('status') === 'CANCELED';
|
||||
}
|
||||
}
|
||||
73
app/Models/EvaluationRunEvent.php
Normal file
73
app/Models/EvaluationRunEvent.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\EvaluationRun;
|
||||
|
||||
/**
|
||||
* Model: EvaluationRunEvent
|
||||
*
|
||||
* Účel:
|
||||
* - Reprezentuje jednu auditní / diagnostickou událost vyhodnocovacího běhu
|
||||
* (EvaluationRun).
|
||||
* - Slouží k zaznamenávání průběhu vyhodnocovací pipeline pro účely:
|
||||
* - monitoringu v administraci
|
||||
* - diagnostiky chyb
|
||||
* - auditní stopy (co, kdy a v jakém kroku proběhlo)
|
||||
*
|
||||
* Kontext v architektuře:
|
||||
* - Události jsou vytvářeny během běhu background jobů
|
||||
* (PrepareRunJob, ParseLogJob, MatchQsoGroupJob, …, FinalizeRunJob).
|
||||
* - Jsou úzce svázány s jedním EvaluationRun a nikdy neexistují samostatně.
|
||||
*
|
||||
* Typické použití:
|
||||
* - Informování UI o aktuálním stavu a průběhu vyhodnocení.
|
||||
* - Záznam varování (např. nevalidní logy, ignorované QSO).
|
||||
* - Záznam chyb, které vedly k selhání kroku nebo celého běhu.
|
||||
*
|
||||
* Pole modelu:
|
||||
* - evaluation_run_id:
|
||||
* Reference na vyhodnocovací běh, ke kterému událost patří.
|
||||
* - level:
|
||||
* Úroveň události (např. info / warning / error / debug).
|
||||
* - message:
|
||||
* Lidsky čitelný popis události, vhodný pro zobrazení v UI.
|
||||
* - context:
|
||||
* Strukturovaná doplňující data (JSON), např.:
|
||||
* - identifikátory logů nebo QSO
|
||||
* - technické detaily chyby
|
||||
* - počty zpracovaných záznamů
|
||||
*
|
||||
* Co model NEDĚLÁ:
|
||||
* - neřídí stav EvaluationRun
|
||||
* - neobsahuje byznys logiku vyhodnocení
|
||||
* - neslouží jako systémový log (nahrazuje pouze audit pipeline)
|
||||
*
|
||||
* Zásady návrhu:
|
||||
* - Události mají být zapisovány sekvenčně během běhu pipeline.
|
||||
* - Neměly by se mazat ani přepisovat (append-only charakter).
|
||||
* - Slouží jako autoritativní zdroj informací o průběhu vyhodnocení.
|
||||
*/
|
||||
class EvaluationRunEvent extends Model
|
||||
{
|
||||
protected $table = 'evaluation_run_events';
|
||||
|
||||
protected $fillable = [
|
||||
'evaluation_run_id',
|
||||
'level',
|
||||
'message',
|
||||
'context',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'evaluation_run_id' => 'integer',
|
||||
'context' => 'array',
|
||||
];
|
||||
|
||||
public function evaluationRun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRun::class);
|
||||
}
|
||||
}
|
||||
20
app/Models/File.php
Normal file
20
app/Models/File.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class File extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'path',
|
||||
'filename',
|
||||
'mimetype',
|
||||
'filesize',
|
||||
'hash',
|
||||
'uploaded_by'
|
||||
];
|
||||
}
|
||||
116
app/Models/Log.php
Normal file
116
app/Models/Log.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use App\Models\PowerCategory;
|
||||
|
||||
class Log extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'logs';
|
||||
|
||||
protected $fillable = [
|
||||
'round_id',
|
||||
'file_id',
|
||||
'accepted',
|
||||
'processed',
|
||||
'ip_address',
|
||||
|
||||
'tname',
|
||||
'tdate',
|
||||
'pcall',
|
||||
'pwwlo',
|
||||
'pexch',
|
||||
'psect',
|
||||
'pband',
|
||||
'pclub',
|
||||
'padr1',
|
||||
'padr2',
|
||||
|
||||
'rname',
|
||||
'rcall',
|
||||
'rcoun',
|
||||
'locator',
|
||||
'radr1',
|
||||
'radr2',
|
||||
'rpoco',
|
||||
'rcity',
|
||||
'rphon',
|
||||
'rhbbs',
|
||||
'mope1',
|
||||
'mope2',
|
||||
'stxeq',
|
||||
'srxeq',
|
||||
'sante',
|
||||
'santh',
|
||||
|
||||
'power_watt',
|
||||
'power_category',
|
||||
'power_category_id',
|
||||
'sixhr_category',
|
||||
|
||||
'claimed_qso_count',
|
||||
'claimed_score',
|
||||
'claimed_wwl',
|
||||
'claimed_dxcc',
|
||||
'cqsos',
|
||||
'cqsop',
|
||||
'cwwls',
|
||||
'cwwlb',
|
||||
'cexcs',
|
||||
'cexcb',
|
||||
'cdxcs',
|
||||
'cdxcb',
|
||||
'ctosc',
|
||||
'codxc',
|
||||
|
||||
'remarks',
|
||||
'remarks_eval',
|
||||
'raw_header',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'round_id' => 'integer',
|
||||
'file_id' => 'integer',
|
||||
|
||||
'accepted' => 'boolean',
|
||||
'processed' => 'boolean',
|
||||
|
||||
'power_watt' => 'float',
|
||||
'power_category_id' => 'integer',
|
||||
'sixhr_category' => 'boolean',
|
||||
|
||||
'claimed_qso_count' => 'integer',
|
||||
'claimed_score' => 'integer',
|
||||
];
|
||||
|
||||
public function round(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Round::class);
|
||||
}
|
||||
|
||||
public function file(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(File::class);
|
||||
}
|
||||
|
||||
public function qsos(): HasMany
|
||||
{
|
||||
return $this->hasMany(LogQso::class);
|
||||
}
|
||||
|
||||
public function logResults(): HasMany
|
||||
{
|
||||
return $this->hasMany(LogResult::class);
|
||||
}
|
||||
|
||||
public function powerCategory(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PowerCategory::class);
|
||||
}
|
||||
}
|
||||
70
app/Models/LogOverride.php
Normal file
70
app/Models/LogOverride.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class LogOverride extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'log_overrides';
|
||||
|
||||
protected $fillable = [
|
||||
'evaluation_run_id',
|
||||
'log_id',
|
||||
'forced_log_status',
|
||||
'forced_band_id',
|
||||
'forced_category_id',
|
||||
'forced_power_category_id',
|
||||
'forced_sixhr_category',
|
||||
'forced_power_w',
|
||||
'reason',
|
||||
'context',
|
||||
'created_by_user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'evaluation_run_id' => 'integer',
|
||||
'log_id' => 'integer',
|
||||
'forced_band_id' => 'integer',
|
||||
'forced_category_id' => 'integer',
|
||||
'forced_power_category_id' => 'integer',
|
||||
'forced_sixhr_category' => 'boolean',
|
||||
'forced_power_w' => 'integer',
|
||||
'context' => 'array',
|
||||
'created_by_user_id' => 'integer',
|
||||
];
|
||||
|
||||
public function evaluationRun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRun::class);
|
||||
}
|
||||
|
||||
public function log(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Log::class);
|
||||
}
|
||||
|
||||
public function forcedBand(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Band::class, 'forced_band_id');
|
||||
}
|
||||
|
||||
public function forcedCategory(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class, 'forced_category_id');
|
||||
}
|
||||
|
||||
public function forcedPowerCategory(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PowerCategory::class, 'forced_power_category_id');
|
||||
}
|
||||
|
||||
public function createdByUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||
}
|
||||
}
|
||||
65
app/Models/LogQso.php
Normal file
65
app/Models/LogQso.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class LogQso extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'log_qsos';
|
||||
|
||||
protected $fillable = [
|
||||
'log_id',
|
||||
'qso_index',
|
||||
|
||||
'time_on',
|
||||
'band',
|
||||
'freq_khz',
|
||||
'mode',
|
||||
|
||||
'my_call',
|
||||
'my_rst',
|
||||
'my_serial',
|
||||
'my_locator',
|
||||
|
||||
'dx_call',
|
||||
'dx_rst',
|
||||
'dx_serial',
|
||||
'rx_wwl',
|
||||
'rx_exchange',
|
||||
'mode_code',
|
||||
|
||||
'points',
|
||||
'wwl',
|
||||
'dxcc',
|
||||
'new_exchange',
|
||||
'new_wwl',
|
||||
'new_dxcc',
|
||||
'duplicate_qso',
|
||||
|
||||
'raw_line',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'log_id' => 'integer',
|
||||
'qso_index' => 'integer',
|
||||
|
||||
'time_on' => 'datetime',
|
||||
'freq_khz' => 'integer',
|
||||
'points' => 'integer',
|
||||
|
||||
'new_exchange'=> 'boolean',
|
||||
'new_wwl' => 'boolean',
|
||||
'new_dxcc' => 'boolean',
|
||||
'duplicate_qso'=> 'boolean',
|
||||
];
|
||||
|
||||
public function log(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Log::class);
|
||||
}
|
||||
}
|
||||
125
app/Models/LogResult.php
Normal file
125
app/Models/LogResult.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class LogResult extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'log_results';
|
||||
|
||||
protected $fillable = [
|
||||
'evaluation_run_id',
|
||||
'log_id',
|
||||
|
||||
'band_id',
|
||||
'category_id',
|
||||
'power_category_id',
|
||||
'sixhr_category',
|
||||
'sixhr_ranking_bucket',
|
||||
'operating_window_start_utc',
|
||||
'operating_window_end_utc',
|
||||
'operating_window_2_start_utc',
|
||||
'operating_window_2_end_utc',
|
||||
'operating_window_hours',
|
||||
'operating_window_qso_count',
|
||||
|
||||
'claimed_qso_count',
|
||||
'claimed_score',
|
||||
|
||||
'valid_qso_count',
|
||||
'dupe_qso_count',
|
||||
'busted_qso_count',
|
||||
'other_error_qso_count',
|
||||
'total_qso_count',
|
||||
'discarded_qso_count',
|
||||
'discarded_points',
|
||||
'discarded_qso_percent',
|
||||
'unique_qso_count',
|
||||
|
||||
'official_score',
|
||||
'penalty_score',
|
||||
'base_score',
|
||||
'multiplier_count',
|
||||
'multiplier_score',
|
||||
'score_per_qso',
|
||||
|
||||
'rank_overall',
|
||||
'rank_in_category',
|
||||
'rank_overall_ok',
|
||||
'rank_in_category_ok',
|
||||
|
||||
'status',
|
||||
'status_reason',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'evaluation_run_id' => 'integer',
|
||||
'log_id' => 'integer',
|
||||
'band_id' => 'integer',
|
||||
'category_id' => 'integer',
|
||||
'power_category_id' => 'integer',
|
||||
'sixhr_category' => 'boolean',
|
||||
'sixhr_ranking_bucket' => 'string',
|
||||
'operating_window_start_utc' => 'datetime',
|
||||
'operating_window_end_utc' => 'datetime',
|
||||
'operating_window_2_start_utc' => 'datetime',
|
||||
'operating_window_2_end_utc' => 'datetime',
|
||||
'operating_window_hours' => 'integer',
|
||||
'operating_window_qso_count' => 'integer',
|
||||
|
||||
'claimed_qso_count' => 'integer',
|
||||
'claimed_score' => 'integer',
|
||||
|
||||
'valid_qso_count' => 'integer',
|
||||
'dupe_qso_count' => 'integer',
|
||||
'busted_qso_count' => 'integer',
|
||||
'other_error_qso_count' => 'integer',
|
||||
'total_qso_count' => 'integer',
|
||||
'discarded_qso_count' => 'integer',
|
||||
'discarded_points' => 'integer',
|
||||
'discarded_qso_percent' => 'float',
|
||||
'unique_qso_count' => 'integer',
|
||||
|
||||
'official_score' => 'integer',
|
||||
'penalty_score' => 'integer',
|
||||
'base_score' => 'integer',
|
||||
'multiplier_count' => 'integer',
|
||||
'multiplier_score' => 'integer',
|
||||
'score_per_qso' => 'float',
|
||||
|
||||
'rank_overall' => 'integer',
|
||||
'rank_in_category' => 'integer',
|
||||
'rank_overall_ok' => 'integer',
|
||||
'rank_in_category_ok' => 'integer',
|
||||
];
|
||||
|
||||
public function evaluationRun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRun::class);
|
||||
}
|
||||
|
||||
public function log(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Log::class);
|
||||
}
|
||||
|
||||
public function band(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Band::class);
|
||||
}
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public function powerCategory(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PowerCategory::class);
|
||||
}
|
||||
}
|
||||
52
app/Models/NewsPost.php
Normal file
52
app/Models/NewsPost.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
//use App\Policies\NewsPostPolicy;
|
||||
//use Illuminate\Database\Eloquent\Attributes\UsePolicy;
|
||||
|
||||
//#[UsePolicy(NewsPostPolicy::class)]
|
||||
class NewsPost extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasTranslations;
|
||||
|
||||
protected $table = 'news_posts';
|
||||
|
||||
public array $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
'excerpt'
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'content',
|
||||
'excerpt',
|
||||
'is_published',
|
||||
'published_at',
|
||||
'author_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_published' => 'boolean',
|
||||
'published_at' => 'datetime',
|
||||
'author_id' => 'integer',
|
||||
];
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'author_id');
|
||||
}
|
||||
|
||||
// route model binding přes slug (pro /api/news/{slug})
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'slug';
|
||||
}
|
||||
}
|
||||
19
app/Models/PowerCategory.php
Normal file
19
app/Models/PowerCategory.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PowerCategory extends Model
|
||||
{
|
||||
protected $table = 'power_categories';
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'order',
|
||||
'power_level'
|
||||
];
|
||||
}
|
||||
56
app/Models/QsoOverride.php
Normal file
56
app/Models/QsoOverride.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class QsoOverride extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'qso_overrides';
|
||||
|
||||
protected $fillable = [
|
||||
'evaluation_run_id',
|
||||
'log_qso_id',
|
||||
'forced_matched_log_qso_id',
|
||||
'forced_status',
|
||||
'forced_points',
|
||||
'forced_penalty',
|
||||
'reason',
|
||||
'context',
|
||||
'created_by_user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'evaluation_run_id' => 'integer',
|
||||
'log_qso_id' => 'integer',
|
||||
'forced_matched_log_qso_id' => 'integer',
|
||||
'forced_points' => 'float',
|
||||
'forced_penalty' => 'float',
|
||||
'context' => 'array',
|
||||
'created_by_user_id' => 'integer',
|
||||
];
|
||||
|
||||
public function evaluationRun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRun::class);
|
||||
}
|
||||
|
||||
public function logQso(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(LogQso::class);
|
||||
}
|
||||
|
||||
public function forcedMatchedLogQso(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(LogQso::class, 'forced_matched_log_qso_id');
|
||||
}
|
||||
|
||||
public function createdByUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by_user_id');
|
||||
}
|
||||
}
|
||||
92
app/Models/QsoResult.php
Normal file
92
app/Models/QsoResult.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class QsoResult extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'qso_results';
|
||||
|
||||
protected $fillable = [
|
||||
'evaluation_run_id',
|
||||
'log_qso_id',
|
||||
|
||||
'is_valid',
|
||||
'is_duplicate',
|
||||
'is_nil',
|
||||
'is_busted_call',
|
||||
'is_busted_rst',
|
||||
'is_busted_exchange',
|
||||
'is_time_out_of_window',
|
||||
'is_operating_window_excluded',
|
||||
|
||||
'points',
|
||||
'penalty_points',
|
||||
'distance_km',
|
||||
'time_diff_sec',
|
||||
|
||||
'wwl',
|
||||
'dxcc',
|
||||
'country',
|
||||
'section',
|
||||
|
||||
'matched_qso_id',
|
||||
'matched_log_qso_id',
|
||||
|
||||
'match_type',
|
||||
'match_confidence',
|
||||
'error_flags',
|
||||
'error_code',
|
||||
'error_side',
|
||||
'error_detail',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'evaluation_run_id' => 'integer',
|
||||
'log_qso_id' => 'integer',
|
||||
|
||||
'is_valid' => 'boolean',
|
||||
'is_duplicate' => 'boolean',
|
||||
'is_nil' => 'boolean',
|
||||
'is_busted_call' => 'boolean',
|
||||
'is_busted_rst' => 'boolean',
|
||||
'is_busted_exchange' => 'boolean',
|
||||
'is_time_out_of_window' => 'boolean',
|
||||
'is_operating_window_excluded' => 'boolean',
|
||||
|
||||
'points' => 'integer',
|
||||
'penalty_points' => 'integer',
|
||||
'distance_km' => 'float',
|
||||
'time_diff_sec' => 'integer',
|
||||
|
||||
'matched_qso_id' => 'integer',
|
||||
'matched_log_qso_id' => 'integer',
|
||||
'error_flags' => 'array',
|
||||
];
|
||||
|
||||
public function evaluationRun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRun::class);
|
||||
}
|
||||
|
||||
public function logQso(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(LogQso::class);
|
||||
}
|
||||
|
||||
public function matchedQso(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(LogQso::class, 'matched_qso_id');
|
||||
}
|
||||
|
||||
public function workingQso(): HasOne
|
||||
{
|
||||
return $this->hasOne(WorkingQso::class, 'log_qso_id', 'log_qso_id');
|
||||
}
|
||||
}
|
||||
97
app/Models/Round.php
Normal file
97
app/Models/Round.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Spatie\Translatable\HasTranslations;
|
||||
use App\Models\EvaluationRuleSet;
|
||||
|
||||
class Round extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasTranslations;
|
||||
|
||||
public array $translatable = [
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
protected $table = 'rounds';
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'contest_id',
|
||||
'rule_set_id',
|
||||
'preliminary_evaluation_run_id',
|
||||
'official_evaluation_run_id',
|
||||
'test_evaluation_run_id',
|
||||
'name',
|
||||
'description',
|
||||
'is_active',
|
||||
'is_test',
|
||||
'is_sixhr',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'logs_deadline',
|
||||
'first_check',
|
||||
'second_check',
|
||||
'unique_qso_check',
|
||||
'third_check',
|
||||
'fourth_check',
|
||||
'prelimitary_results',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'contest_id' => 'integer',
|
||||
'rule_set_id' => 'integer',
|
||||
'preliminary_evaluation_run_id' => 'integer',
|
||||
'official_evaluation_run_id' => 'integer',
|
||||
'test_evaluation_run_id' => 'integer',
|
||||
|
||||
'name' => 'array',
|
||||
'description' => 'array',
|
||||
|
||||
'is_active' => 'boolean',
|
||||
'is_test' => 'boolean',
|
||||
'is_sixhr' => 'boolean',
|
||||
|
||||
'start_time' => 'datetime',
|
||||
'end_time' => 'datetime',
|
||||
'logs_deadline' => 'datetime',
|
||||
|
||||
'first_check' => 'datetime',
|
||||
'second_check' => 'datetime',
|
||||
'unique_qso_check' => 'datetime',
|
||||
'third_check' => 'datetime',
|
||||
'fourth_check' => 'datetime',
|
||||
'prelimitary_results'=> 'datetime',
|
||||
];
|
||||
|
||||
public function contest(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Contest::class);
|
||||
}
|
||||
|
||||
public function ruleSet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id');
|
||||
}
|
||||
|
||||
public function bands(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Band::class, 'rounds_bands', 'round_id', 'band_id');
|
||||
}
|
||||
|
||||
public function categories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Category::class, 'rounds_categories', 'round_id', 'category_id');
|
||||
}
|
||||
|
||||
public function powerCategories(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(PowerCategory::class, 'rounds_power_categories', 'round_id', 'power_category_id');
|
||||
}
|
||||
}
|
||||
53
app/Models/User.php
Normal file
53
app/Models/User.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable, HasApiTokens;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'is_admin',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_admin' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
56
app/Models/WorkingQso.php
Normal file
56
app/Models/WorkingQso.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WorkingQso extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'working_qsos';
|
||||
|
||||
protected $fillable = [
|
||||
'evaluation_run_id',
|
||||
'log_qso_id',
|
||||
'log_id',
|
||||
'ts_utc',
|
||||
'call_norm',
|
||||
'rcall_norm',
|
||||
'loc_norm',
|
||||
'rloc_norm',
|
||||
'band_id',
|
||||
'mode',
|
||||
'match_key',
|
||||
'dupe_key',
|
||||
'out_of_window',
|
||||
'errors',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'evaluation_run_id' => 'integer',
|
||||
'log_qso_id' => 'integer',
|
||||
'log_id' => 'integer',
|
||||
'band_id' => 'integer',
|
||||
'out_of_window' => 'boolean',
|
||||
'errors' => 'array',
|
||||
'ts_utc' => 'datetime',
|
||||
];
|
||||
|
||||
public function evaluationRun(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvaluationRun::class);
|
||||
}
|
||||
|
||||
public function logQso(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(LogQso::class);
|
||||
}
|
||||
|
||||
public function log(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Log::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user