Nezobrazovat detail logu anonymnímu uživateli #2 - i v tabulce s deklarovanými výsledky byl lokátor a utíkaly informace závodníkům před uzávěrkou.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\LogResult;
|
use App\Models\LogResult;
|
||||||
|
use App\Models\Log;
|
||||||
use App\Models\EvaluationRun;
|
use App\Models\EvaluationRun;
|
||||||
use App\Models\Round;
|
use App\Models\Round;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -35,7 +36,7 @@ class LogResultController extends BaseController
|
|||||||
$query = LogResult::query()
|
$query = LogResult::query()
|
||||||
->with([
|
->with([
|
||||||
'evaluationRun.ruleSet:id,sixhr_ranking_mode',
|
'evaluationRun.ruleSet:id,sixhr_ranking_mode',
|
||||||
'log',
|
'log.round',
|
||||||
'band:id,name,order',
|
'band:id,name,order',
|
||||||
'category:id,name,order',
|
'category:id,name,order',
|
||||||
'powerCategory:id,name,order',
|
'powerCategory:id,name,order',
|
||||||
@@ -116,6 +117,16 @@ class LogResultController extends BaseController
|
|||||||
->orderByDesc('official_score')
|
->orderByDesc('official_score')
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|
||||||
|
if ($this->shouldRedactDeclaredResults($request)) {
|
||||||
|
$items->getCollection()->transform(function (LogResult $item) {
|
||||||
|
if ($item->log) {
|
||||||
|
$item->log->pwwlo = null;
|
||||||
|
$item->log->codxc = null;
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json($items);
|
return response()->json($items);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,12 +160,20 @@ class LogResultController extends BaseController
|
|||||||
{
|
{
|
||||||
$logResult->load([
|
$logResult->load([
|
||||||
'evaluationRun.ruleSet:id,sixhr_ranking_mode',
|
'evaluationRun.ruleSet:id,sixhr_ranking_mode',
|
||||||
'log',
|
'log.round',
|
||||||
'band:id,name,order',
|
'band:id,name,order',
|
||||||
'category:id,name,order',
|
'category:id,name,order',
|
||||||
'powerCategory:id,name,order',
|
'powerCategory:id,name,order',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$request = request();
|
||||||
|
if ($this->shouldRedactDeclaredResults($request, $logResult)) {
|
||||||
|
if ($logResult->log) {
|
||||||
|
$logResult->log->pwwlo = null;
|
||||||
|
$logResult->log->codxc = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json($logResult);
|
return response()->json($logResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,4 +255,51 @@ class LogResultController extends BaseController
|
|||||||
'status_reason' => ['sometimes', 'nullable', 'string'],
|
'status_reason' => ['sometimes', 'nullable', 'string'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function shouldRedactDeclaredResults(Request $request, ?LogResult $logResult = null): bool
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
if ($user && $user->is_admin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($logResult) {
|
||||||
|
$run = $logResult->evaluationRun;
|
||||||
|
$isClaimed = $run && strtoupper((string) $run->rules_version) === 'CLAIMED';
|
||||||
|
if (! $isClaimed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ! $this->hasOfficialResultsPublished($logResult->log?->round);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->get('status') !== 'CLAIMED') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$round = null;
|
||||||
|
if ($request->filled('round_id')) {
|
||||||
|
$round = Round::find((int) $request->get('round_id'));
|
||||||
|
} elseif ($request->filled('evaluation_run_id')) {
|
||||||
|
$run = EvaluationRun::find((int) $request->get('evaluation_run_id'));
|
||||||
|
$round = $run?->round;
|
||||||
|
} elseif ($request->filled('log_id')) {
|
||||||
|
$log = Log::with('round')->find((int) $request->get('log_id'));
|
||||||
|
$round = $log?->round;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! $this->hasOfficialResultsPublished($round);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasOfficialResultsPublished(?Round $round): bool
|
||||||
|
{
|
||||||
|
if (! $round || ! $round->official_evaluation_run_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EvaluationRun::query()
|
||||||
|
->where('id', $round->official_evaluation_run_id)
|
||||||
|
->where('status', 'SUCCEEDED')
|
||||||
|
->where('result_type', 'FINAL')
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ type ResultsTablesProps = {
|
|||||||
onResultTypeChange?: (label: string | null, className: string, resultType: string | null) => void;
|
onResultTypeChange?: (label: string | null, className: string, resultType: string | null) => void;
|
||||||
refreshKey?: string | number | null;
|
refreshKey?: string | number | null;
|
||||||
evaluationRunId?: number | null;
|
evaluationRunId?: number | null;
|
||||||
|
isFinalPublished?: boolean;
|
||||||
|
isAdmin?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +72,8 @@ export default function ResultsTables({
|
|||||||
onResultTypeChange,
|
onResultTypeChange,
|
||||||
refreshKey = null,
|
refreshKey = null,
|
||||||
evaluationRunId = null,
|
evaluationRunId = null,
|
||||||
|
isFinalPublished = false,
|
||||||
|
isAdmin = false,
|
||||||
}: ResultsTablesProps) {
|
}: ResultsTablesProps) {
|
||||||
const [items, setItems] = useState<LogResultItem[]>([]);
|
const [items, setItems] = useState<LogResultItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -86,6 +90,8 @@ export default function ResultsTables({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const showScoreTotal = mode === "final";
|
||||||
|
const showLocatorOdx = mode === "final" || isAdmin || isFinalPublished;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!roundId) return;
|
if (!roundId) return;
|
||||||
@@ -251,12 +257,16 @@ export default function ResultsTables({
|
|||||||
const headerColumns = [
|
const headerColumns = [
|
||||||
<TableColumn key="rank" className="whitespace-nowrap">{t("results_table_rank") ?? "Pořadí"}</TableColumn>,
|
<TableColumn key="rank" className="whitespace-nowrap">{t("results_table_rank") ?? "Pořadí"}</TableColumn>,
|
||||||
<TableColumn key="callsign" className="whitespace-nowrap">{t("results_table_callsign") ?? "Značka v závodě"}</TableColumn>,
|
<TableColumn key="callsign" className="whitespace-nowrap">{t("results_table_callsign") ?? "Značka v závodě"}</TableColumn>,
|
||||||
<TableColumn key="locator" className="whitespace-nowrap">{t("results_table_locator") ?? "Lokátor"}</TableColumn>,
|
showLocatorOdx
|
||||||
|
? <TableColumn key="locator" className="whitespace-nowrap">{t("results_table_locator") ?? "Lokátor"}</TableColumn>
|
||||||
|
: null,
|
||||||
<TableColumn key="category" className="whitespace-nowrap">{t("results_table_category") ?? "Kategorie"}</TableColumn>,
|
<TableColumn key="category" className="whitespace-nowrap">{t("results_table_category") ?? "Kategorie"}</TableColumn>,
|
||||||
<TableColumn key="band" className="whitespace-nowrap">{t("results_table_band") ?? "Pásmo"}</TableColumn>,
|
<TableColumn key="band" className="whitespace-nowrap">{t("results_table_band") ?? "Pásmo"}</TableColumn>,
|
||||||
<TableColumn key="power_watt" className="whitespace-nowrap">{t("results_table_power_watt") ?? "Výkon [W]"}</TableColumn>,
|
<TableColumn key="power_watt" className="whitespace-nowrap">{t("results_table_power_watt") ?? "Výkon [W]"}</TableColumn>,
|
||||||
<TableColumn key="power_category" className="whitespace-nowrap">{t("results_table_power_category") ?? "Výkonová kat."}</TableColumn>,
|
<TableColumn key="power_category" className="whitespace-nowrap">{t("results_table_power_category") ?? "Výkonová kat."}</TableColumn>,
|
||||||
<TableColumn key="score_total" className="whitespace-nowrap">{t("results_table_score_total") ?? "Body celkem"}</TableColumn>,
|
showScoreTotal
|
||||||
|
? <TableColumn key="score_total" className="whitespace-nowrap">{t("results_table_score_total") ?? "Body celkem"}</TableColumn>
|
||||||
|
: null,
|
||||||
<TableColumn key="claimed_score" className="whitespace-nowrap">{t("results_table_claimed_score") ?? "Deklarované body"}</TableColumn>,
|
<TableColumn key="claimed_score" className="whitespace-nowrap">{t("results_table_claimed_score") ?? "Deklarované body"}</TableColumn>,
|
||||||
<TableColumn key="qso_count" className="whitespace-nowrap">{t("results_table_qso_count") ?? "Počet QSO"}</TableColumn>,
|
<TableColumn key="qso_count" className="whitespace-nowrap">{t("results_table_qso_count") ?? "Počet QSO"}</TableColumn>,
|
||||||
showCalculatedColumns
|
showCalculatedColumns
|
||||||
@@ -275,7 +285,9 @@ export default function ResultsTables({
|
|||||||
? <TableColumn key="unique_qso" className="whitespace-nowrap">{t("results_table_unique_qso") ?? "Unique QSO"}</TableColumn>
|
? <TableColumn key="unique_qso" className="whitespace-nowrap">{t("results_table_unique_qso") ?? "Unique QSO"}</TableColumn>
|
||||||
: null,
|
: null,
|
||||||
<TableColumn key="score_per_qso" className="whitespace-nowrap">{t("results_table_score_per_qso") ?? "Body / QSO"}</TableColumn>,
|
<TableColumn key="score_per_qso" className="whitespace-nowrap">{t("results_table_score_per_qso") ?? "Body / QSO"}</TableColumn>,
|
||||||
<TableColumn key="odx" className="whitespace-nowrap">{t("results_table_odx") ?? "ODX"}</TableColumn>,
|
showLocatorOdx
|
||||||
|
? <TableColumn key="odx" className="whitespace-nowrap">{t("results_table_odx") ?? "ODX"}</TableColumn>
|
||||||
|
: null,
|
||||||
<TableColumn key="antenna" className="whitespace-nowrap">{t("results_table_antenna") ?? "Anténa"}</TableColumn>,
|
<TableColumn key="antenna" className="whitespace-nowrap">{t("results_table_antenna") ?? "Anténa"}</TableColumn>,
|
||||||
<TableColumn key="antenna_height" className="whitespace-nowrap">{t("results_table_antenna_height") ?? "Ant. height"}</TableColumn>,
|
<TableColumn key="antenna_height" className="whitespace-nowrap">{t("results_table_antenna_height") ?? "Ant. height"}</TableColumn>,
|
||||||
showCalculatedColumns
|
showCalculatedColumns
|
||||||
@@ -293,7 +305,7 @@ export default function ResultsTables({
|
|||||||
<span className="text-md font-semibold">{heading}</span>
|
<span className="text-md font-semibold">{heading}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => exportCsv(heading, sorted, rankField, mode)}
|
onClick={() => exportCsv(heading, sorted, rankField, mode, showLocatorOdx, showScoreTotal)}
|
||||||
className="text-xs text-primary-600 hover:text-primary-700 underline"
|
className="text-xs text-primary-600 hover:text-primary-700 underline"
|
||||||
>
|
>
|
||||||
CSV
|
CSV
|
||||||
@@ -326,7 +338,9 @@ export default function ResultsTables({
|
|||||||
<TableCell key="callsign" className={callsignClassName}>
|
<TableCell key="callsign" className={callsignClassName}>
|
||||||
{item.log?.pcall ?? "—"}
|
{item.log?.pcall ?? "—"}
|
||||||
</TableCell>,
|
</TableCell>,
|
||||||
<TableCell key="locator" className="whitespace-nowrap">{item.log?.pwwlo ?? "—"}</TableCell>,
|
showLocatorOdx
|
||||||
|
? <TableCell key="locator" className="whitespace-nowrap">{item.log?.pwwlo ?? "—"}</TableCell>
|
||||||
|
: null,
|
||||||
<TableCell key="category" className="whitespace-nowrap">
|
<TableCell key="category" className="whitespace-nowrap">
|
||||||
{item.category?.name ?? "—"}
|
{item.category?.name ?? "—"}
|
||||||
</TableCell>,
|
</TableCell>,
|
||||||
@@ -339,7 +353,9 @@ export default function ResultsTables({
|
|||||||
<TableCell key="power_category" className="whitespace-nowrap">
|
<TableCell key="power_category" className="whitespace-nowrap">
|
||||||
{item.power_category?.name ?? "—"}
|
{item.power_category?.name ?? "—"}
|
||||||
</TableCell>,
|
</TableCell>,
|
||||||
<TableCell key="score_total" className="whitespace-nowrap">{formatScore(item, mode)}</TableCell>,
|
showScoreTotal
|
||||||
|
? <TableCell key="score_total" className="whitespace-nowrap">{formatScore(item, mode)}</TableCell>
|
||||||
|
: null,
|
||||||
<TableCell key="claimed_score" className="whitespace-nowrap">{formatNumber(item.claimed_score)}</TableCell>,
|
<TableCell key="claimed_score" className="whitespace-nowrap">{formatNumber(item.claimed_score)}</TableCell>,
|
||||||
<TableCell key="qso_count" className="whitespace-nowrap">{formatQsoCount(item, mode)}</TableCell>,
|
<TableCell key="qso_count" className="whitespace-nowrap">{formatQsoCount(item, mode)}</TableCell>,
|
||||||
showCalculatedColumns
|
showCalculatedColumns
|
||||||
@@ -352,7 +368,9 @@ export default function ResultsTables({
|
|||||||
? <TableCell key="unique_qso" className="whitespace-nowrap">{formatNumber(item.unique_qso_count)}</TableCell>
|
? <TableCell key="unique_qso" className="whitespace-nowrap">{formatNumber(item.unique_qso_count)}</TableCell>
|
||||||
: null,
|
: null,
|
||||||
<TableCell key="score_per_qso" className="whitespace-nowrap">{formatScorePerQso(item.score_per_qso)}</TableCell>,
|
<TableCell key="score_per_qso" className="whitespace-nowrap">{formatScorePerQso(item.score_per_qso)}</TableCell>,
|
||||||
<TableCell key="odx" className="whitespace-nowrap">{item.log?.codxc ?? "—"}</TableCell>,
|
showLocatorOdx
|
||||||
|
? <TableCell key="odx" className="whitespace-nowrap">{item.log?.codxc ?? "—"}</TableCell>
|
||||||
|
: null,
|
||||||
<TableCell key="antenna" className="whitespace-nowrap">{item.log?.sante ?? "—"}</TableCell>,
|
<TableCell key="antenna" className="whitespace-nowrap">{item.log?.sante ?? "—"}</TableCell>,
|
||||||
<TableCell key="antenna_height" className="whitespace-nowrap">{item.log?.santh ?? "—"}</TableCell>,
|
<TableCell key="antenna_height" className="whitespace-nowrap">{item.log?.santh ?? "—"}</TableCell>,
|
||||||
showCalculatedColumns
|
showCalculatedColumns
|
||||||
@@ -580,22 +598,29 @@ function isPowerA(item: LogResultItem) {
|
|||||||
return name === "a";
|
return name === "a";
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportCsv(title: string, rows: LogResultItem[], rankField: RankField, mode: "claimed" | "final") {
|
function exportCsv(
|
||||||
|
title: string,
|
||||||
|
rows: LogResultItem[],
|
||||||
|
rankField: RankField,
|
||||||
|
mode: "claimed" | "final",
|
||||||
|
showLocatorOdx: boolean,
|
||||||
|
showScoreTotal: boolean
|
||||||
|
) {
|
||||||
const includeCalculated = mode === "final";
|
const includeCalculated = mode === "final";
|
||||||
const header = [
|
const header = [
|
||||||
"Poradi",
|
"Poradi",
|
||||||
"Značka v závodě",
|
"Značka v závodě",
|
||||||
"Lokátor",
|
...(showLocatorOdx ? ["Lokátor"] : []),
|
||||||
"Kategorie",
|
"Kategorie",
|
||||||
"Pásmo",
|
"Pásmo",
|
||||||
"Výkon [W]",
|
"Výkon [W]",
|
||||||
"Výkonová kat.",
|
"Výkonová kat.",
|
||||||
"Body celkem",
|
...(showScoreTotal ? ["Body celkem"] : []),
|
||||||
"Deklarované body",
|
"Deklarované body",
|
||||||
"Počet QSO",
|
"Počet QSO",
|
||||||
...(includeCalculated ? ["Vyřazeno QSO", "Vyřazeno bodů", "Unique QSO"] : []),
|
...(includeCalculated ? ["Vyřazeno QSO", "Vyřazeno bodů", "Unique QSO"] : []),
|
||||||
"Body / QSO",
|
"Body / QSO",
|
||||||
"ODX",
|
...(showLocatorOdx ? ["ODX"] : []),
|
||||||
"Anténa",
|
"Anténa",
|
||||||
"Ant. height",
|
"Ant. height",
|
||||||
];
|
];
|
||||||
@@ -605,31 +630,32 @@ function exportCsv(title: string, rows: LogResultItem[], rankField: RankField, m
|
|||||||
const qsoCount = getQsoCount(r, mode);
|
const qsoCount = getQsoCount(r, mode);
|
||||||
const ratio = formatScorePerQso(r.score_per_qso);
|
const ratio = formatScorePerQso(r.score_per_qso);
|
||||||
const discardedQso = formatDiscardedQso(r);
|
const discardedQso = formatDiscardedQso(r);
|
||||||
const base = [
|
const base: Array<string | number> = [];
|
||||||
r[rankField] ?? "",
|
base.push(r[rankField] ?? "");
|
||||||
r.log?.pcall ?? "",
|
base.push(r.log?.pcall ?? "");
|
||||||
r.log?.pwwlo ?? "",
|
if (showLocatorOdx) {
|
||||||
r.category?.name ?? "",
|
base.push(r.log?.pwwlo ?? "");
|
||||||
r.band?.name ?? "",
|
|
||||||
r.log?.power_watt ?? "",
|
|
||||||
r.power_category?.name ?? "",
|
|
||||||
score ?? "",
|
|
||||||
r.claimed_score ?? "",
|
|
||||||
qsoCount ?? "",
|
|
||||||
ratio === "—" ? "" : ratio,
|
|
||||||
r.log?.codxc ?? "",
|
|
||||||
r.log?.sante ?? "",
|
|
||||||
r.log?.santh ?? "",
|
|
||||||
];
|
|
||||||
if (includeCalculated) {
|
|
||||||
base.splice(
|
|
||||||
10,
|
|
||||||
0,
|
|
||||||
discardedQso === "—" ? "" : discardedQso,
|
|
||||||
r.discarded_points ?? "",
|
|
||||||
r.unique_qso_count ?? ""
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
base.push(r.category?.name ?? "");
|
||||||
|
base.push(r.band?.name ?? "");
|
||||||
|
base.push(r.log?.power_watt ?? "");
|
||||||
|
base.push(r.power_category?.name ?? "");
|
||||||
|
if (showScoreTotal) {
|
||||||
|
base.push(score ?? "");
|
||||||
|
}
|
||||||
|
base.push(r.claimed_score ?? "");
|
||||||
|
base.push(qsoCount ?? "");
|
||||||
|
if (includeCalculated) {
|
||||||
|
base.push(discardedQso === "—" ? "" : discardedQso);
|
||||||
|
base.push(r.discarded_points ?? "");
|
||||||
|
base.push(r.unique_qso_count ?? "");
|
||||||
|
}
|
||||||
|
base.push(ratio === "—" ? "" : ratio);
|
||||||
|
if (showLocatorOdx) {
|
||||||
|
base.push(r.log?.codxc ?? "");
|
||||||
|
}
|
||||||
|
base.push(r.log?.sante ?? "");
|
||||||
|
base.push(r.log?.santh ?? "");
|
||||||
return base.map((v) => `"${String(v).replace(/"/g, '""')}"`).join(",");
|
return base.map((v) => `"${String(v).replace(/"/g, '""')}"`).join(",");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import ResultsTables from "@/components/ResultsTables";
|
|||||||
import RoundEvaluationPanel from "@/components/RoundEvaluationPanel";
|
import RoundEvaluationPanel from "@/components/RoundEvaluationPanel";
|
||||||
import { Button, Card, CardHeader, CardBody, Divider, Tabs, Tab } from "@heroui/react";
|
import { Button, Card, CardHeader, CardBody, Divider, Tabs, Tab } from "@heroui/react";
|
||||||
import { useContestStore } from "@/stores/contestStore";
|
import { useContestStore } from "@/stores/contestStore";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useUserStore } from "@/stores/userStore";
|
import { useUserStore } from "@/stores/userStore";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -21,7 +21,7 @@ export default function RoundDetailPage() {
|
|||||||
const user = useUserStore((s) => s.user);
|
const user = useUserStore((s) => s.user);
|
||||||
const [logsRefreshKey, setLogsRefreshKey] = useState(0);
|
const [logsRefreshKey, setLogsRefreshKey] = useState(0);
|
||||||
const selectedTab = searchParams.get("tab") ?? "verified";
|
const selectedTab = searchParams.get("tab") ?? "verified";
|
||||||
const [declaredFilter, setDeclaredFilter] = useState<"ALL" | "OK">("ALL");
|
const [declaredFilter, setDeclaredFilter] = useState<"ALL" | "OK">("OK");
|
||||||
const [finalFilter, setFinalFilter] = useState<"ALL" | "OK">("ALL");
|
const [finalFilter, setFinalFilter] = useState<"ALL" | "OK">("ALL");
|
||||||
const [finalResultLabel, setFinalResultLabel] = useState<string | null>(null);
|
const [finalResultLabel, setFinalResultLabel] = useState<string | null>(null);
|
||||||
const [finalResultClass, setFinalResultClass] = useState<string>("");
|
const [finalResultClass, setFinalResultClass] = useState<string>("");
|
||||||
@@ -29,8 +29,10 @@ export default function RoundDetailPage() {
|
|||||||
const [recalcLoading, setRecalcLoading] = useState(false);
|
const [recalcLoading, setRecalcLoading] = useState(false);
|
||||||
const [recalcMessage, setRecalcMessage] = useState<string | null>(null);
|
const [recalcMessage, setRecalcMessage] = useState<string | null>(null);
|
||||||
const [recalcError, setRecalcError] = useState<string | null>(null);
|
const [recalcError, setRecalcError] = useState<string | null>(null);
|
||||||
|
const [finalPublished, setFinalPublished] = useState(false);
|
||||||
const { run } = useRoundEvaluationRun(rId);
|
const { run } = useRoundEvaluationRun(rId);
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
|
const isAdmin = !!user?.is_admin;
|
||||||
const roundDeadline =
|
const roundDeadline =
|
||||||
selectedRound?.id === rId ? selectedRound?.logs_deadline ?? null : null;
|
selectedRound?.id === rId ? selectedRound?.logs_deadline ?? null : null;
|
||||||
const anonymousUploadClosed = () => {
|
const anonymousUploadClosed = () => {
|
||||||
@@ -78,6 +80,45 @@ export default function RoundDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAdmin) {
|
||||||
|
setFinalPublished(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!rId) {
|
||||||
|
setFinalPublished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const officialRunId =
|
||||||
|
selectedRound?.id === rId ? selectedRound?.official_evaluation_run_id ?? null : null;
|
||||||
|
if (!officialRunId) {
|
||||||
|
setFinalPublished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let active = true;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get<{ status?: string | null; result_type?: string | null }>(
|
||||||
|
`/api/evaluation-runs/${officialRunId}`,
|
||||||
|
{
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
withCredentials: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!active) return;
|
||||||
|
const isFinal =
|
||||||
|
res.data?.status === "SUCCEEDED" && res.data?.result_type === "FINAL";
|
||||||
|
setFinalPublished(isFinal);
|
||||||
|
} catch {
|
||||||
|
if (!active) return;
|
||||||
|
setFinalPublished(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [rId, selectedRound?.official_evaluation_run_id, isAdmin]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -127,16 +168,6 @@ export default function RoundDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mb-3 flex items-center gap-3 text-sm">
|
<div className="mb-3 flex items-center gap-3 text-sm">
|
||||||
<label className="flex items-center gap-1 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="finalFilter"
|
|
||||||
value="ALL"
|
|
||||||
checked={finalFilter === "ALL"}
|
|
||||||
onChange={() => setFinalFilter("ALL")}
|
|
||||||
/>
|
|
||||||
<span>{filterAllLabel}</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-1 cursor-pointer">
|
<label className="flex items-center gap-1 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -147,6 +178,16 @@ export default function RoundDetailPage() {
|
|||||||
/>
|
/>
|
||||||
<span>{filterOkLabel}</span>
|
<span>{filterOkLabel}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="flex items-center gap-1 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="finalFilter"
|
||||||
|
value="ALL"
|
||||||
|
checked={finalFilter === "ALL"}
|
||||||
|
onChange={() => setFinalFilter("ALL")}
|
||||||
|
/>
|
||||||
|
<span>{filterAllLabel}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<ResultsTables
|
<ResultsTables
|
||||||
roundId={rId}
|
roundId={rId}
|
||||||
@@ -154,6 +195,8 @@ export default function RoundDetailPage() {
|
|||||||
filter={finalFilter}
|
filter={finalFilter}
|
||||||
mode="final"
|
mode="final"
|
||||||
showResultTypeLabel={false}
|
showResultTypeLabel={false}
|
||||||
|
isFinalPublished={finalPublished}
|
||||||
|
isAdmin={isAdmin}
|
||||||
onResultTypeChange={(label, className, resultType) => {
|
onResultTypeChange={(label, className, resultType) => {
|
||||||
setFinalResultLabel(label);
|
setFinalResultLabel(label);
|
||||||
setFinalResultClass(className);
|
setFinalResultClass(className);
|
||||||
@@ -174,16 +217,6 @@ export default function RoundDetailPage() {
|
|||||||
<p>{t("declared_note_line3") ?? "Deklarované výsledky jsou uspořádány na základě údajů v hlavičce EDI souborů v řádce CQSOP=. Další sloupce rovněž zobrazují data z deníku, které jsou kontrolovány pouze na správnost formátu zápisu."}</p>
|
<p>{t("declared_note_line3") ?? "Deklarované výsledky jsou uspořádány na základě údajů v hlavičce EDI souborů v řádce CQSOP=. Další sloupce rovněž zobrazují data z deníku, které jsou kontrolovány pouze na správnost formátu zápisu."}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center gap-3 text-sm">
|
<div className="mb-3 flex items-center gap-3 text-sm">
|
||||||
<label className="flex items-center gap-1 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="declaredFilter"
|
|
||||||
value="ALL"
|
|
||||||
checked={declaredFilter === "ALL"}
|
|
||||||
onChange={() => setDeclaredFilter("ALL")}
|
|
||||||
/>
|
|
||||||
<span>{filterAllLabel}</span>
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center gap-1 cursor-pointer">
|
<label className="flex items-center gap-1 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -194,6 +227,16 @@ export default function RoundDetailPage() {
|
|||||||
/>
|
/>
|
||||||
<span>{filterOkLabel}</span>
|
<span>{filterOkLabel}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="flex items-center gap-1 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="declaredFilter"
|
||||||
|
value="ALL"
|
||||||
|
checked={declaredFilter === "ALL"}
|
||||||
|
onChange={() => setDeclaredFilter("ALL")}
|
||||||
|
/>
|
||||||
|
<span>{filterAllLabel}</span>
|
||||||
|
</label>
|
||||||
{user && (
|
{user && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -208,7 +251,13 @@ export default function RoundDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
{recalcMessage && <div className="mb-2 text-sm text-green-600">{recalcMessage}</div>}
|
{recalcMessage && <div className="mb-2 text-sm text-green-600">{recalcMessage}</div>}
|
||||||
{recalcError && <div className="mb-2 text-sm text-red-600">{recalcError}</div>}
|
{recalcError && <div className="mb-2 text-sm text-red-600">{recalcError}</div>}
|
||||||
<ResultsTables roundId={rId} contestId={cId} filter={declaredFilter} />
|
<ResultsTables
|
||||||
|
roundId={rId}
|
||||||
|
contestId={cId}
|
||||||
|
filter={declaredFilter}
|
||||||
|
isFinalPublished={finalPublished}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key="logs" title={t("uploaded_logs_tab") ?? "Nahrané logy"}>
|
<Tab key="logs" title={t("uploaded_logs_tab") ?? "Nahrané logy"}>
|
||||||
|
|||||||
@@ -138,4 +138,90 @@ class LogResultControllerTest extends TestCase
|
|||||||
'log_id' => $log->id,
|
'log_id' => $log->id,
|
||||||
])->assertStatus(403);
|
])->assertStatus(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_claimed_results_redact_locator_and_odx_for_anonymous(): void
|
||||||
|
{
|
||||||
|
$round = $this->createRound();
|
||||||
|
$claimedRun = $this->createEvaluationRun([
|
||||||
|
'round_id' => $round->id,
|
||||||
|
'rules_version' => 'CLAIMED',
|
||||||
|
]);
|
||||||
|
$log = $this->createLog([
|
||||||
|
'round_id' => $round->id,
|
||||||
|
'pwwlo' => 'JN79',
|
||||||
|
'codxc' => 'OK2XYZ',
|
||||||
|
]);
|
||||||
|
$result = $this->createLogResult([
|
||||||
|
'evaluation_run_id' => $claimedRun->id,
|
||||||
|
'log_id' => $log->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson("/api/log-results?round_id={$round->id}&status=CLAIMED");
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$row = collect($response->json('data'))->firstWhere('id', $result->id);
|
||||||
|
$this->assertNotNull($row);
|
||||||
|
$this->assertNull($row['log']['pwwlo']);
|
||||||
|
$this->assertNull($row['log']['codxc']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_sees_locator_and_odx_for_claimed_results(): void
|
||||||
|
{
|
||||||
|
$this->actingAsAdmin();
|
||||||
|
$round = $this->createRound();
|
||||||
|
$claimedRun = $this->createEvaluationRun([
|
||||||
|
'round_id' => $round->id,
|
||||||
|
'rules_version' => 'CLAIMED',
|
||||||
|
]);
|
||||||
|
$log = $this->createLog([
|
||||||
|
'round_id' => $round->id,
|
||||||
|
'pwwlo' => 'JN79',
|
||||||
|
'codxc' => 'OK2XYZ',
|
||||||
|
]);
|
||||||
|
$result = $this->createLogResult([
|
||||||
|
'evaluation_run_id' => $claimedRun->id,
|
||||||
|
'log_id' => $log->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson("/api/log-results?round_id={$round->id}&status=CLAIMED");
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$row = collect($response->json('data'))->firstWhere('id', $result->id);
|
||||||
|
$this->assertNotNull($row);
|
||||||
|
$this->assertSame('JN79', $row['log']['pwwlo']);
|
||||||
|
$this->assertSame('OK2XYZ', $row['log']['codxc']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_claimed_result_show_reveals_locator_after_final_published(): void
|
||||||
|
{
|
||||||
|
$round = $this->createRound();
|
||||||
|
$claimedRun = $this->createEvaluationRun([
|
||||||
|
'round_id' => $round->id,
|
||||||
|
'rules_version' => 'CLAIMED',
|
||||||
|
]);
|
||||||
|
$officialRun = $this->createEvaluationRun([
|
||||||
|
'round_id' => $round->id,
|
||||||
|
'rules_version' => 'OFFICIAL',
|
||||||
|
'status' => 'SUCCEEDED',
|
||||||
|
'result_type' => 'FINAL',
|
||||||
|
]);
|
||||||
|
$round->official_evaluation_run_id = $officialRun->id;
|
||||||
|
$round->save();
|
||||||
|
|
||||||
|
$log = $this->createLog([
|
||||||
|
'round_id' => $round->id,
|
||||||
|
'pwwlo' => 'JN79',
|
||||||
|
'codxc' => 'OK2XYZ',
|
||||||
|
]);
|
||||||
|
$result = $this->createLogResult([
|
||||||
|
'evaluation_run_id' => $claimedRun->id,
|
||||||
|
'log_id' => $log->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson("/api/log-results/{$result->id}");
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$this->assertSame('JN79', $response->json('log.pwwlo'));
|
||||||
|
$this->assertSame('OK2XYZ', $response->json('log.codxc'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user