From 537b998e8059c56e3a20ee2a89d42c3bbfbb80b8 Mon Sep 17 00:00:00 2001
From: Daniel Karbach
Date: Wed, 19 Oct 2022 15:39:54 +0200
Subject: [PATCH] open tournament type
---
app/Events/ResultChanged.php | 1 +
app/Http/Controllers/ResultController.php | 16 ++--
app/Http/Controllers/RoundController.php | 6 +-
app/Http/Controllers/TournamentController.php | 1 +
app/Models/Result.php | 35 +++++++
app/Models/Round.php | 92 ++++++++++++-------
app/Models/Tournament.php | 4 +
.../2022_10_19_072222_tournament_type.php | 32 +++++++
.../js/components/results/DetailDialog.js | 18 ++--
resources/js/components/results/Item.js | 18 ++--
resources/js/components/results/List.js | 20 +++-
.../js/components/results/ReportButton.js | 16 ++--
.../js/components/results/ReportDialog.js | 8 +-
resources/js/components/results/ReportForm.js | 12 +--
resources/js/components/rounds/Item.js | 7 +-
resources/js/components/tournament/Detail.js | 19 ++--
.../components/tournament/SettingsDialog.js | 19 ++--
resources/js/helpers/Round.js | 1 +
resources/js/helpers/Tournament.js | 6 ++
resources/js/helpers/permissions.js | 7 +-
20 files changed, 234 insertions(+), 104 deletions(-)
create mode 100644 database/migrations/2022_10_19_072222_tournament_type.php
diff --git a/app/Events/ResultChanged.php b/app/Events/ResultChanged.php
index 4a62d9d..da93768 100644
--- a/app/Events/ResultChanged.php
+++ b/app/Events/ResultChanged.php
@@ -23,6 +23,7 @@ class ResultChanged implements ShouldBroadcast
public function __construct(Result $result)
{
$this->result = $result;
+ $result->load('user');
}
/**
diff --git a/app/Http/Controllers/ResultController.php b/app/Http/Controllers/ResultController.php
index 8698e19..57f78a1 100644
--- a/app/Http/Controllers/ResultController.php
+++ b/app/Http/Controllers/ResultController.php
@@ -4,10 +4,10 @@ namespace App\Http\Controllers;
use App\Events\ResultChanged;
use App\Models\DiscordBotCommand;
-use App\Models\Participant;
use App\Models\Protocol;
use App\Models\Result;
use App\Models\Round;
+use App\Models\User;
use Illuminate\Http\Request;
class ResultController extends Controller
@@ -17,22 +17,20 @@ class ResultController extends Controller
$validatedData = $request->validate([
'comment' => 'string',
'forfeit' => 'boolean',
- 'participant_id' => 'required|exists:App\\Models\\Participant,id',
'round_id' => 'required|exists:App\\Models\\Round,id',
'time' => 'numeric',
+ 'user_id' => 'required|exists:App\\Models\\User,id',
]);
- $participant = Participant::findOrFail($validatedData['participant_id']);
$round = Round::findOrFail($validatedData['round_id']);
- $user = $request->user();
- if ($user->id != $participant->user->id) {
+ if ($validatedData['user_id'] != $request->user()->id) {
$this->authorize('create', Result::class);
}
$result = Result::firstOrCreate([
'round_id' => $validatedData['round_id'],
- 'user_id' => $participant->user_id,
+ 'user_id' => $validatedData['user_id'],
]);
if (!$round->locked) {
if (isset($validatedData['forfeit'])) $result->forfeit = $validatedData['forfeit'];
@@ -62,7 +60,11 @@ class ResultController extends Controller
$round->load('results');
$round->updatePlacement();
- $round->tournament->updatePlacement();
+ if ($round->tournament->hasScoreboard()) {
+ $round->tournament->updatePlacement();
+ }
+
+ $result->load('user');
return $result->toJson();
}
diff --git a/app/Http/Controllers/RoundController.php b/app/Http/Controllers/RoundController.php
index 596628d..04e1b86 100644
--- a/app/Http/Controllers/RoundController.php
+++ b/app/Http/Controllers/RoundController.php
@@ -57,7 +57,7 @@ class RoundController extends Controller
RoundChanged::dispatch($round);
- $round->load('results');
+ $round->load(['results', 'results.user']);
return $round->toJson();
}
@@ -76,7 +76,7 @@ class RoundController extends Controller
RoundChanged::dispatch($round);
- $round->load('results');
+ $round->load(['results', 'results.user']);
return $round->toJson();
}
@@ -95,7 +95,7 @@ class RoundController extends Controller
RoundChanged::dispatch($round);
- $round->load('results');
+ $round->load(['results', 'results.user']);
return $round->toJson();
}
diff --git a/app/Http/Controllers/TournamentController.php b/app/Http/Controllers/TournamentController.php
index 2bb258e..9b21736 100644
--- a/app/Http/Controllers/TournamentController.php
+++ b/app/Http/Controllers/TournamentController.php
@@ -30,6 +30,7 @@ class TournamentController extends Controller
'applications.user',
'rounds',
'rounds.results',
+ 'rounds.results.user',
'participants',
'participants.user',
)->findOrFail($id);
diff --git a/app/Models/Result.php b/app/Models/Result.php
index 99f3791..c672a6a 100644
--- a/app/Models/Result.php
+++ b/app/Models/Result.php
@@ -11,6 +11,37 @@ class Result extends Model
use HasFactory;
+ public static function compareResult(Result $a, Result $b) {
+ $a_time = !$a->forfeit ? $a->time : 0;
+ $b_time = !$b->forfeit ? $b->time : 0;
+ if ($a_time) {
+ if ($b_time) {
+ if ($a_time < $b_time) return -1;
+ if ($b_time < $a_time) return 1;
+ return static::compareUsername($a, $b);
+ }
+ return -1;
+ }
+ if ($b_time) {
+ return 1;
+ }
+ if ($a->forfeit) {
+ if ($b->forfeit) {
+ return static::compareUsername($a, $b);
+ }
+ return -1;
+ }
+ if ($b->forfeit) {
+ return 1;
+ }
+ return static::compareUsername($a, $b);
+ }
+
+ public static function compareUsername(Participant $a, Participant $b) {
+ return strcasecmp($a->user->username, $b->user->username);
+ }
+
+
public function formatTime() {
$hours = floor($this->time / 60 / 60);
$minutes = floor(($this->time / 60) % 60);
@@ -45,6 +76,10 @@ class Result extends Model
return $this->belongsTo(Participant::class);
}
+ public function user() {
+ return $this->belongsTo(User::class);
+ }
+
public function getHasFinishedAttribute() {
return $this->time > 0 || $this->forfeit;
}
diff --git a/app/Models/Round.php b/app/Models/Round.php
index b6c9d02..0f4847a 100644
--- a/app/Models/Round.php
+++ b/app/Models/Round.php
@@ -21,43 +21,67 @@ class Round extends Model
}
public function updatePlacement() {
- $runners = [];
- foreach ($this->tournament->participants as $p) {
- if ($p->isRunner()) {
- $runners[] = $p;
- } else {
- $result = $p->findResult($this);
- if ($result) {
- $result->updatePlacement(null, null);
+ if ($this->tournament->type == 'open-async') {
+ $results = $this->results->sort([Result::class, 'compareResult']);
+ $reversed = $results->reverse();
+
+ $running = 0;
+ $bonus = 1;
+ $lastResult = null;
+ foreach ($reversed as $result) {
+ $betterThanLast = is_null($lastResult) || $result->time < $lastResult;
+ if (!$result->forfeit && $betterThanLast) {
+ $running += $bonus;
+ $lastResult = $result->time;
+ $bonus = 1;
+ } else {
+ ++$bonus;
+ }
+ if (!$result->forfeit) {
+ $result->updatePlacement($running, count($results) - $running + 1);
+ } else {
+ $result->updatePlacement(0, count($results));
}
}
- }
-
- usort($runners, Participant::compareResult($this));
- $mapped = array_map(function ($p) {
- return ['participant' => $p, 'result' => $p->findResult($this)];
- }, $runners);
- $filtered = array_filter($mapped, function($r) {
- return $r['result'] && ($r['result']->time || $r['result']->forfeit);
- });
- $reversed = array_reverse($filtered);
-
- $running = 0;
- $bonus = 1;
- $lastResult = null;
- foreach ($reversed as $r) {
- $betterThanLast = is_null($lastResult) || $r['result']->time < $lastResult;
- if (!$r['result']->forfeit && $betterThanLast) {
- $running += $bonus;
- $lastResult = $r['result']->time;
- $bonus = 1;
- } else {
- ++$bonus;
+ } else {
+ $runners = [];
+ foreach ($this->tournament->participants as $p) {
+ if ($p->isRunner()) {
+ $runners[] = $p;
+ } else {
+ $result = $p->findResult($this);
+ if ($result) {
+ $result->updatePlacement(null, null);
+ }
+ }
}
- if (!$r['result']->forfeit) {
- $r['result']->updatePlacement($running, count($filtered) - $running + 1);
- } else {
- $r['result']->updatePlacement(0, count($filtered));
+
+ usort($runners, Participant::compareResult($this));
+ $mapped = array_map(function ($p) {
+ return ['participant' => $p, 'result' => $p->findResult($this)];
+ }, $runners);
+ $filtered = array_filter($mapped, function($r) {
+ return $r['result'] && ($r['result']->time || $r['result']->forfeit);
+ });
+ $reversed = array_reverse($filtered);
+
+ $running = 0;
+ $bonus = 1;
+ $lastResult = null;
+ foreach ($reversed as $r) {
+ $betterThanLast = is_null($lastResult) || $r['result']->time < $lastResult;
+ if (!$r['result']->forfeit && $betterThanLast) {
+ $running += $bonus;
+ $lastResult = $r['result']->time;
+ $bonus = 1;
+ } else {
+ ++$bonus;
+ }
+ if (!$r['result']->forfeit) {
+ $r['result']->updatePlacement($running, count($filtered) - $running + 1);
+ } else {
+ $r['result']->updatePlacement(0, count($filtered));
+ }
}
}
}
diff --git a/app/Models/Tournament.php b/app/Models/Tournament.php
index eea8809..6f0feb1 100644
--- a/app/Models/Tournament.php
+++ b/app/Models/Tournament.php
@@ -21,6 +21,10 @@ class Tournament extends Model
return $runners;
}
+ public function hasScoreboard() {
+ return $this->type == 'signup-async';
+ }
+
public function updatePlacement() {
$runners = [];
foreach ($this->participants as $p) {
diff --git a/database/migrations/2022_10_19_072222_tournament_type.php b/database/migrations/2022_10_19_072222_tournament_type.php
new file mode 100644
index 0000000..8f05554
--- /dev/null
+++ b/database/migrations/2022_10_19_072222_tournament_type.php
@@ -0,0 +1,32 @@
+string('type')->default('signup-async');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('tournaments', function(Blueprint $table) {
+ $table->dropColumn('type');
+ });
+ }
+};
diff --git a/resources/js/components/results/DetailDialog.js b/resources/js/components/results/DetailDialog.js
index 368575b..a914f5d 100644
--- a/resources/js/components/results/DetailDialog.js
+++ b/resources/js/components/results/DetailDialog.js
@@ -5,8 +5,8 @@ import { withTranslation } from 'react-i18next';
import Box from '../users/Box';
import { getTime } from '../../helpers/Result';
-import { findResult } from '../../helpers/Participant';
import { maySeeResults } from '../../helpers/permissions';
+import { findResult } from '../../helpers/User';
import { withUser } from '../../helpers/UserContext';
import i18n from '../../i18n';
@@ -14,15 +14,15 @@ const getPlacement = result =>
`${result.placement}. (${i18n.t('results.points', { count: result.score })})`;
const DetailDialog = ({
+ authUser,
onHide,
- participant,
round,
show,
tournament,
user,
}) => {
- const result = findResult(participant, round);
- const maySee = maySeeResults(user, tournament, round);
+ const result = findResult(user, round);
+ const maySee = maySeeResults(authUser, tournament, round);
return
@@ -41,7 +41,7 @@ const DetailDialog = ({
{i18n.t('results.runner')}
-
+
{i18n.t('results.result')}
@@ -76,11 +76,9 @@ const DetailDialog = ({
};
DetailDialog.propTypes = {
- onHide: PropTypes.func,
- participant: PropTypes.shape({
- user: PropTypes.shape({
- }),
+ authUser: PropTypes.shape({
}),
+ onHide: PropTypes.func,
round: PropTypes.shape({
created_at: PropTypes.string,
number: PropTypes.number,
@@ -92,4 +90,4 @@ DetailDialog.propTypes = {
}),
};
-export default withTranslation()(withUser(DetailDialog));
+export default withTranslation()(withUser(DetailDialog, 'authUser'));
diff --git a/resources/js/components/results/Item.js b/resources/js/components/results/Item.js
index a2ee8e3..4b243b3 100644
--- a/resources/js/components/results/Item.js
+++ b/resources/js/components/results/Item.js
@@ -5,8 +5,8 @@ import { Button } from 'react-bootstrap';
import DetailDialog from './DetailDialog';
import Box from '../users/Box';
import { getIcon, getTime } from '../../helpers/Result';
-import { findResult } from '../../helpers/Participant';
import { maySeeResults } from '../../helpers/permissions';
+import { findResult } from '../../helpers/User';
import { withUser } from '../../helpers/UserContext';
const getClassName = result => {
@@ -23,16 +23,16 @@ const getClassName = result => {
};
const Item = ({
- participant,
+ authUser,
round,
tournament,
user,
}) => {
const [showDialog, setShowDialog] = useState(false);
- const result = findResult(participant, round);
- const maySee = maySeeResults(user, tournament, round);
+ const result = findResult(user, round);
+ const maySee = maySeeResults(authUser, tournament, round);
return
-
+
setShowDialog(false)}
- participant={participant}
round={round}
show={showDialog}
tournament={tournament}
+ user={user}
/>
;
};
Item.propTypes = {
- participant: PropTypes.shape({
- user: PropTypes.shape({
- }),
+ authUser: PropTypes.shape({
}),
round: PropTypes.shape({
}),
@@ -66,4 +64,4 @@ Item.propTypes = {
}),
};
-export default withUser(Item);
+export default withUser(Item, 'authUser');
diff --git a/resources/js/components/results/List.js b/resources/js/components/results/List.js
index 14c7096..3d1bdfa 100644
--- a/resources/js/components/results/List.js
+++ b/resources/js/components/results/List.js
@@ -8,6 +8,19 @@ import { getRunners } from '../../helpers/Tournament';
import { withUser } from '../../helpers/UserContext';
const List = ({ round, tournament, user }) => {
+ if (tournament.type === 'open-async') {
+ const results = round.results || [];
+ return
+ {results.map(result =>
+
+ )}
+
;
+ }
const runners = maySeeResults(user, tournament, round)
? sortByResult(getRunners(tournament), round)
: sortByFinished(getRunners(tournament), round);
@@ -15,9 +28,9 @@ const List = ({ round, tournament, user }) => {
{runners.map(participant =>
)}
;
@@ -25,10 +38,15 @@ const List = ({ round, tournament, user }) => {
List.propTypes = {
round: PropTypes.shape({
+ results: PropTypes.arrayOf(PropTypes.shape({
+ })),
}),
tournament: PropTypes.shape({
participants: PropTypes.arrayOf(PropTypes.shape({
})),
+ type: PropTypes.string,
+ users: PropTypes.arrayOf(PropTypes.shape({
+ })),
}),
user: PropTypes.shape({
}),
diff --git a/resources/js/components/results/ReportButton.js b/resources/js/components/results/ReportButton.js
index 0a8c139..ec8f8fd 100644
--- a/resources/js/components/results/ReportButton.js
+++ b/resources/js/components/results/ReportButton.js
@@ -5,11 +5,11 @@ import { withTranslation } from 'react-i18next';
import ReportDialog from './ReportDialog';
import Icon from '../common/Icon';
-import { findResult } from '../../helpers/Participant';
+import { findResult } from '../../helpers/User';
import i18n from '../../i18n';
-const getButtonLabel = (participant, round) => {
- const result = findResult(participant, round);
+const getButtonLabel = (user, round) => {
+ const result = findResult(user, round);
if (round.locked) {
if (result && result.comment) {
return i18n.t('results.editComment');
@@ -25,7 +25,7 @@ const getButtonLabel = (participant, round) => {
}
};
-const ReportButton = ({ participant, round }) => {
+const ReportButton = ({ round, user }) => {
const [showDialog, setShowDialog] = useState(false);
return <>
@@ -33,26 +33,26 @@ const ReportButton = ({ participant, round }) => {
onClick={() => setShowDialog(true)}
variant="secondary"
>
- {getButtonLabel(participant, round)}
+ {getButtonLabel(user, round)}
{' '}
setShowDialog(false)}
- participant={participant}
round={round}
show={showDialog}
+ user={user}
/>
>;
};
ReportButton.propTypes = {
- participant: PropTypes.shape({
- }),
round: PropTypes.shape({
}),
tournament: PropTypes.shape({
}),
+ user: PropTypes.shape({
+ }),
};
export default withTranslation()(ReportButton);
diff --git a/resources/js/components/results/ReportDialog.js b/resources/js/components/results/ReportDialog.js
index 62adf84..b3e5282 100644
--- a/resources/js/components/results/ReportDialog.js
+++ b/resources/js/components/results/ReportDialog.js
@@ -8,9 +8,9 @@ import i18n from '../../i18n';
const ReportDialog = ({
onHide,
- participant,
round,
show,
+ user,
}) =>
@@ -20,20 +20,20 @@ const ReportDialog = ({
;
ReportDialog.propTypes = {
onHide: PropTypes.func,
- participant: PropTypes.shape({
- }),
round: PropTypes.shape({
}),
show: PropTypes.bool,
tournament: PropTypes.shape({
}),
+ user: PropTypes.shape({
+ }),
};
export default withTranslation()(ReportDialog);
diff --git a/resources/js/components/results/ReportForm.js b/resources/js/components/results/ReportForm.js
index 942869d..9940171 100644
--- a/resources/js/components/results/ReportForm.js
+++ b/resources/js/components/results/ReportForm.js
@@ -8,7 +8,7 @@ import toastr from 'toastr';
import LargeCheck from '../common/LargeCheck';
import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
-import { findResult } from '../../helpers/Participant';
+import { findResult } from '../../helpers/User';
import { formatTime, parseTime } from '../../helpers/Result';
import i18n from '../../i18n';
import yup from '../../schema/yup';
@@ -121,16 +121,16 @@ export default withFormik({
displayName: 'ReportForm',
enableReinitialize: true,
handleSubmit: async (values, actions) => {
- const { comment, forfeit, participant_id, round_id, time } = values;
+ const { comment, forfeit, round_id, time, user_id } = values;
const { setErrors } = actions;
const { onCancel } = actions.props;
try {
await axios.post('/api/results', {
comment,
forfeit,
- participant_id,
round_id,
time: parseTime(time) || 0,
+ user_id,
});
toastr.success(i18n.t('results.reportSuccess'));
if (onCancel) {
@@ -143,14 +143,14 @@ export default withFormik({
}
}
},
- mapPropsToValues: ({ participant, round }) => {
- const result = findResult(participant, round);
+ mapPropsToValues: ({ round, user }) => {
+ const result = findResult(user, round);
return {
comment: result && result.comment ? result.comment : '',
forfeit: result ? !!result.forfeit : false,
- participant_id: participant.id,
round_id: round.id,
time: result && result.time ? formatTime(result) : '',
+ user_id: user.id,
};
},
validationSchema: yup.object().shape({
diff --git a/resources/js/components/rounds/Item.js b/resources/js/components/rounds/Item.js
index 6eb3d94..1b1edb2 100644
--- a/resources/js/components/rounds/Item.js
+++ b/resources/js/components/rounds/Item.js
@@ -8,9 +8,8 @@ import SeedCode from './SeedCode';
import SeedRolledBy from './SeedRolledBy';
import List from '../results/List';
import ReportButton from '../results/ReportButton';
-import { isRunner } from '../../helpers/permissions';
+import { mayReportResult, isRunner } from '../../helpers/permissions';
import { isComplete } from '../../helpers/Round';
-import { findParticipant } from '../../helpers/Tournament';
import { hasFinishedRound } from '../../helpers/User';
import { withUser } from '../../helpers/UserContext';
import i18n from '../../i18n';
@@ -60,12 +59,12 @@ const Item = ({
{' '}
- {isRunner(user, tournament) ?
+ {mayReportResult(user, tournament) ?
: null}
diff --git a/resources/js/components/tournament/Detail.js b/resources/js/components/tournament/Detail.js
index 66b425e..bb9b928 100644
--- a/resources/js/components/tournament/Detail.js
+++ b/resources/js/components/tournament/Detail.js
@@ -21,6 +21,7 @@ import {
getTournamentAdmins,
getTournamentMonitors,
hasRunners,
+ hasScoreboard,
hasTournamentAdmins,
hasTournamentMonitors,
} from '../../helpers/Tournament';
@@ -65,15 +66,17 @@ const Detail = ({
-
-
{i18n.t('tournaments.scoreboard')}
- {hasRunners(tournament) && tournament.rounds.length > 2 ?
-
+ {hasScoreboard(tournament) ? <>
+
+
{i18n.t('tournaments.scoreboard')}
+ {hasRunners(tournament) && tournament.rounds.length > 2 ?
+
+ : null}
+
+ {hasRunners(tournament) ?
+
: null}
-
- {hasRunners(tournament) ?
-
- : null}
+ > : null}
{hasTournamentAdmins(tournament) ?
<>
diff --git a/resources/js/components/tournament/SettingsDialog.js b/resources/js/components/tournament/SettingsDialog.js
index af4d664..362cd6f 100644
--- a/resources/js/components/tournament/SettingsDialog.js
+++ b/resources/js/components/tournament/SettingsDialog.js
@@ -9,6 +9,7 @@ import DiscordForm from './DiscordForm';
import DiscordSelect from '../common/DiscordSelect';
import Icon from '../common/Icon';
import ToggleSwitch from '../common/ToggleSwitch';
+import Tournament from '../../helpers/Tournament';
import i18n from '../../i18n';
const open = async tournament => {
@@ -77,14 +78,16 @@ const SettingsDialog = ({
-
- {i18n.t('tournaments.open')}
- value
- ? open(tournament) : close(tournament)}
- value={tournament.accept_applications}
- />
-
+ {Tournament.hasSignup(tournament) ?
+
+ {i18n.t('tournaments.open')}
+ value
+ ? open(tournament) : close(tournament)}
+ value={tournament.accept_applications}
+ />
+
+ : null}
{i18n.t('tournaments.locked')}
{
if (!tournament || !tournament.participants) return false;
if (!round || !round.results) return false;
const runners = Tournament.getRunners(tournament);
+ if (!runners.length) return false;
for (let i = 0; i < runners.length; ++i) {
const result = Participant.findResult(runners[i], round);
if (!result || !result.has_finished) return false;
diff --git a/resources/js/helpers/Tournament.js b/resources/js/helpers/Tournament.js
index 8f2239f..9d2962d 100644
--- a/resources/js/helpers/Tournament.js
+++ b/resources/js/helpers/Tournament.js
@@ -30,6 +30,10 @@ export const getRunners = tournament => {
.sort(Participant.compareUsername);
};
+export const hasScoreboard = tournament => !!(tournament && tournament.type === 'signup-async');
+
+export const hasSignup = tournament => !!(tournament && tournament.type === 'signup-async');
+
export const getScoreTable = tournament => {
if (!tournament || !tournament.rounds || !tournament.rounds.length) return [];
const runners = getRunners(tournament);
@@ -190,6 +194,8 @@ export default {
getTournamentCrew,
getTournamentMonitors,
hasRunners,
+ hasScoreboard,
+ hasSignup,
hasTournamentAdmins,
hasTournamentCrew,
hasTournamentMonitors,
diff --git a/resources/js/helpers/permissions.js b/resources/js/helpers/permissions.js
index 4ce6935..d761325 100644
--- a/resources/js/helpers/permissions.js
+++ b/resources/js/helpers/permissions.js
@@ -61,6 +61,12 @@ export const mayApply = (user, tournament) =>
export const mayHandleApplications = (user, tournament) =>
tournament && tournament.accept_applications && isTournamentAdmin(user, tournament);
+export const mayReportResult = (user, tournament) => {
+ if (!user || !tournament) return false;
+ if (tournament.type === 'open-async') return true;
+ return isRunner(user, tournament);
+};
+
export const mayLockRound = (user, tournament) =>
!tournament.locked && isTournamentAdmin(user, tournament);
@@ -78,7 +84,6 @@ export const maySeeResults = (user, tournament, round) =>
round.locked ||
hasFinished(user, round) ||
isTournamentMonitor(user, tournament) ||
- (isTournamentAdmin(user, tournament) && !isRunner(user, tournament)) ||
Round.isComplete(tournament, round);
// Users
--
2.39.2