]> git.localhorst.tv Git - alttp.git/commitdiff
options when to reveal round results
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 7 May 2025 13:45:40 +0000 (15:45 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 7 May 2025 13:45:40 +0000 (15:45 +0200)
app/Events/RoundChanged.php
app/Http/Controllers/TournamentController.php
app/Models/Round.php
app/Models/Tournament.php
app/Policies/RoundPolicy.php
database/migrations/2025_05_07_123722_add_tournament_result_reveal_column.php [new file with mode: 0644]
resources/js/components/tournament/SettingsDialog.js
resources/js/helpers/Round.js
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js

index a4df19961938f870b2c117821cf2764c6c9845c5..926e2e26b5656c2f6911f7732729f1a527fe127c 100644 (file)
@@ -23,6 +23,7 @@ class RoundChanged implements ShouldBroadcast
        public function __construct(Round $round)
        {
                $this->round = $round;
+               $this->round->setRelations([]);
        }
 
        /**
index 2dc7e51a84ae104597626dd05950b4383c55e873..5ce63f457c1389702894118e8a93e654d9c4d2e2 100644 (file)
@@ -105,11 +105,15 @@ class TournamentController extends Controller
        public function settings(Request $request, Tournament $tournament) {
                $this->authorize('update', $tournament);
                $validatedData = $request->validate([
+                       'result_reveal' => 'string|nullable|in:always,finishers,never,participants',
                        'show_numbers' => 'boolean|nullable',
                ]);
                if (array_key_exists('show_numbers', $validatedData)) {
                        $tournament->show_numbers = $validatedData['show_numbers'];
                }
+               if (isset($validatedData['result_reveal'])) {
+                       $tournament->result_reveal = $validatedData['result_reveal'];
+               }
                $tournament->save();
                if ($tournament->wasChanged()) {
                        TournamentChanged::dispatch($tournament);
index 7daa77957da58769f332421c6b7840407badb74e..e5ce7034f7c6c3e5dfde139d659a003006475258 100644 (file)
@@ -12,6 +12,7 @@ class Round extends Model
 
        public function isComplete() {
                if (count($this->tournament->participants) == 0) return false;
+               if ($this->tournament->type == 'open-async') return false;
                if (count($this->results) == 0) return false;
                foreach ($this->tournament->getRunners() as $participant) {
                        $result = $participant->findResult($this);
index 96e89b8939358aea1b88acfdfa8f1c89270e86aa..2ba33f17eef65953578c49ca1cc7d7942885a550 100644 (file)
@@ -2,7 +2,6 @@
 
 namespace App\Models;
 
-use App\Events\ParticipantChanged;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 
@@ -48,7 +47,6 @@ class Tournament extends Model
                }
 
                usort($runners, [Participant::class, 'compareScore']);
-               $reversed = array_reverse($runners);
                $placement = count($runners);
                $skipped = 0;
                $lastScore = $runners[0]->score;
index 1f872c57bf054edaaf7de6fbba5ea60857c1e40b..5bb46c073b69f97abd40515640e9c465d3827123 100644 (file)
@@ -101,11 +101,20 @@ class RoundPolicy
         */
        public function seeResults(?User $user, Round $round)
        {
-               return
+               if (
                        $round->locked ||
-                       ($user && $user->hasFinished($round)) ||
                        ($user && $user->isTournamentMonitor($round->tournament)) ||
-                       $round->isComplete();
+                       $round->isComplete()
+               ) {
+                       return true;
+               }
+               if ($round->tournament->result_reveal == 'finishers') {
+                       return ($user && $user->hasFinished($round));
+               }
+               if ($round->tournament->result_reveal == 'participants') {
+                       return ($user && $user->isRunner($round->tournament));
+               }
+               return false;
        }
 
        /**
diff --git a/database/migrations/2025_05_07_123722_add_tournament_result_reveal_column.php b/database/migrations/2025_05_07_123722_add_tournament_result_reveal_column.php
new file mode 100644 (file)
index 0000000..5c60b81
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        */
+       public function up(): void
+       {
+               Schema::table('tournaments', function(Blueprint $table) {
+                       $table->string('result_reveal')->default('finishers');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        */
+       public function down(): void
+       {
+               Schema::table('tournaments', function(Blueprint $table) {
+                       $table->dropColumn('result_reveal');
+               });
+       }
+};
index 46fbfd87ecae299ec5aef94bddebbf71c691af9b..a40a0a1d76c639af30dc8a5fa6dd037db612e12a 100644 (file)
@@ -1,7 +1,7 @@
 import axios from 'axios';
 import PropTypes from 'prop-types';
 import React from 'react';
-import { Button, Col, Modal, Row } from 'react-bootstrap';
+import { Button, Col, Form, Modal, Row } from 'react-bootstrap';
 import { withTranslation } from 'react-i18next';
 import toastr from 'toastr';
 
@@ -113,6 +113,27 @@ const SettingsDialog = ({
                                                value={tournament.show_numbers}
                                        />
                                </div>
+                               <div className="d-flex align-items-center justify-content-between mb-3">
+                                       <span title={i18n.t('tournaments.resultRevealDescription')}>
+                                               {i18n.t('tournaments.resultReveal')}
+                                       </span>
+                                       <Form.Select
+                                               onChange={({ target: { value } }) =>
+                                                       settings(tournament, { result_reveal: value })}
+                                               style={{ width: '50%' }}
+                                               value={tournament.result_reveal}
+                                       >
+                                               {['never', 'finishers', 'participants', 'always'].map((key) =>
+                                                       <option
+                                                               key={key}
+                                                               title={i18n.t(`tournaments.resultRevealOptionDescription.${key}`)}
+                                                               value={key}
+                                                       >
+                                                               {i18n.t(`tournaments.resultRevealOption.${key}`)}
+                                                       </option>
+                                               )}
+                                       </Form.Select>
+                               </div>
                                <div className="d-flex align-items-center justify-content-between">
                                        <div>
                                                <p>{i18n.t('tournaments.discord')}</p>
@@ -157,6 +178,7 @@ SettingsDialog.propTypes = {
                accept_applications: PropTypes.bool,
                discord: PropTypes.string,
                locked: PropTypes.bool,
+               result_reveal: PropTypes.string,
                show_numbers: PropTypes.bool,
        }),
 };
index c7bb811a9c581f80cea1224db8d7d1c35e8d87d8..429d9fa95fdc56a25e3c4cd360af0354ff93bb9f 100644 (file)
@@ -3,6 +3,7 @@ import Tournament from './Tournament';
 
 export const isComplete = (tournament, round) => {
        if (!tournament || !tournament.participants) return false;
+       if (tournament.type === 'open-async') return false;
        if (!round || !round.results) return false;
        const runners = Tournament.getRunners(tournament);
        if (!runners.length) return false;
index 23aba6955b2eb2783894497f85bf881ce43bca66..c24d75a97eafebb0eebfa80b42fb8cac2b4e693f 100644 (file)
@@ -106,14 +106,14 @@ export const isTournamentAdmin = (user, tournament) => {
        return p && p.roles && p.roles.includes('admin');
 };
 
-export const isTournamentCrew = (user, tournament) =>
-       isTournamentAdmin(user, tournament) || isTournamentMonitor(user, tournament);
-
 export const isTournamentMonitor = (user, tournament) => {
        const p = isParticipant(user, tournament);
        return p && p.roles && p.roles.includes('monitor');
 };
 
+export const isTournamentCrew = (user, tournament) =>
+       isTournamentAdmin(user, tournament) || isTournamentMonitor(user, tournament);
+
 export const hasFinished = (user, round) =>
        user && round && round.results &&
        round.results.find(r => r.user_id == user.id && r.has_finished);
@@ -151,11 +151,22 @@ export const mayUpdateTournament = (user, tournament) =>
 export const mayViewProtocol = (user, tournament) =>
        isTournamentCrew(user, tournament);
 
-export const maySeeResults = (user, tournament, round) =>
-       round.locked ||
-       hasFinished(user, round) ||
-       isTournamentMonitor(user, tournament) ||
-       Round.isComplete(tournament, round);
+export const maySeeResults = (user, tournament, round) => {
+       if (
+               round.locked ||
+               isTournamentMonitor(user, tournament) ||
+               Round.isComplete(tournament, round)
+       ) {
+               return true;
+       }
+       if (tournament.result_reveal === 'finishers') {
+               return hasFinished(user, round);
+       }
+       if (tournament.result_reveal === 'participants') {
+               return isRunner(user, tournament);
+       }
+       return false;
+};
 
 // Twitch
 
index 90922a6fda76cc9921a512a46a54790e830275c5..658a2f6f339d120029b1cd48a8b806c6c91606f7 100644 (file)
@@ -690,6 +690,20 @@ export default {
                        open: 'Anmeldung geöffnet',
                        openError: 'Fehler beim Öffnen der Anmledung',
                        openSuccess: 'Anmeldung geöffnet',
+                       resultReveal: 'Frühzeitige Ergebnisse',
+                       resultRevealDescription: 'Ob und wann Ergebnisse von offenen Runden angezeigt werden sollen.',
+                       resultRevealOption: {
+                               always: 'Immer',
+                               finishers: 'Absolventen',
+                               never: 'Nie',
+                               participants: 'Teilnehmern',
+                       },
+                       resultRevealOptionDescription: {
+                               always: 'Ergebnis wird allen angezeigt, sobald es eingetragen wird.',
+                               finishers: 'Ergebnis wird allen Teilnehmern angezeigt, die eine Zeit (oder DNF) für die Runde eingetragen haben.',
+                               never: 'Ergebnis wird erst mit Abschluss der Runde veröffentlicht.',
+                               participants: 'Ergebnis wird allen registrierten Teilnehmern des Turniers angezeigt.',
+                       },
                        scoreboard: 'Scoreboard',
                        scoreChart: 'Turnierverlauf',
                        settings: 'Einstellungen',
index f961bf0080b26eea7c013a6f3ecae46354681cdf..1740cf64ef31919264ce99f9aac66ecffa744c9d 100644 (file)
@@ -690,6 +690,20 @@ export default {
                        open: 'Open registration',
                        openError: 'Error opening registration',
                        openSuccess: 'Registration opened',
+                       resultReveal: 'Early results',
+                       resultRevealDescription: 'When to show results for open rounds.',
+                       resultRevealOption: {
+                               always: 'Always',
+                               finishers: 'Finishers',
+                               never: 'Never',
+                               participants: 'Participants',
+                       },
+                       resultRevealOptionDescription: {
+                               always: 'Show all results as they come in.',
+                               finishers: 'Show results to participants who submitted a time (or DNF).',
+                               never: 'Only reveal results once the round has closed down.',
+                               participants: 'Show results to only registered tournament members.',
+                       },
                        scoreboard: 'Scoreboard',
                        scoreChart: 'Score chart',
                        settings: 'Settings',