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