]> git.localhorst.tv Git - alttp.git/commitdiff
track seed downloads
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 19 Nov 2025 15:21:58 +0000 (16:21 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 19 Nov 2025 15:21:58 +0000 (16:21 +0100)
12 files changed:
app/Http/Controllers/RoundController.php
app/Models/Protocol.php
app/Models/Round.php
app/Policies/RoundPolicy.php
database/migrations/2025_11_19_150210_tournament_require_auth.php [new file with mode: 0644]
resources/js/components/common/Icon.jsx
resources/js/components/protocol/Item.jsx
resources/js/components/rounds/SeedButton.jsx
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js
routes/web.php

index 589ab8d93e0d6e032673bdeada67710ca815f45b..1459f52a38693e080d0a59d9f1ab857359379bb4 100644 (file)
@@ -117,6 +117,20 @@ class RoundController extends Controller
                return $round->toJson();
        }
 
+       public function getSeed(Request $request, Round $round) {
+               $this->authorize('getSeed', $round);
+
+               if ($request->user()) {
+                       Protocol::roundSeedGet(
+                               $round->tournament,
+                               $round,
+                               $request->user(),
+                       );
+               }
+
+               return redirect($round->seed);
+       }
+
        public function uploadSeed(Request $request, Round $round) {
                $this->authorize('update', $round);
 
index 73dca93d7a4f1444a997b5f90a0b3ff79837023e..4eddf715be2bf7ea8b98675eed0e97c04e02a6a9 100644 (file)
@@ -132,6 +132,19 @@ class Protocol extends Model
                ProtocolAdded::dispatch($protocol);
        }
 
+       public static function roundSeedGet(Tournament $tournament, Round $round, User $user) {
+               $protocol = static::create([
+                       'tournament_id' => $tournament->id,
+                       'user_id' => $user->id,
+                       'type' => 'round.getseed',
+                       'details' => [
+                               'tournament' => static::tournamentMemo($tournament),
+                               'round' => static::roundMemo($round),
+                       ],
+               ]);
+               ProtocolAdded::dispatch($protocol);
+       }
+
        public static function roundSeedSet(Tournament $tournament, Round $round, User $user) {
                $protocol = static::create([
                        'tournament_id' => $tournament->id,
index 480832209850be0c9450568ca7c70e7a4ea9d439..05195776bc0e487c8aee7dc7e5307b57f5ccade5 100644 (file)
@@ -4,6 +4,7 @@ namespace App\Models;
 
 use Illuminate\Broadcasting\Channel;
 use Illuminate\Database\Eloquent\BroadcastsEvents;
+use Illuminate\Database\Eloquent\Casts\Attribute;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 
@@ -107,6 +108,12 @@ class Round extends Model
                }
        }
 
+    protected function hasSeed(): Attribute {
+        return Attribute::make(
+            get: fn () => !!$this->seed,
+        );
+    }
+
 
        public function results() {
                return $this->hasMany(Result::class);
@@ -120,6 +127,9 @@ class Round extends Model
                return $this->belongsTo(Tournament::class);
        }
 
+       protected $appends = [
+               'has_seed',
+       ];
 
        protected $casts = [
                'code' => 'array',
index 7f53e0fe56a362a01d1d887b8946d50687e0f0bf..0d928ddd662f43980816fed7c532810be846210f 100644 (file)
@@ -132,6 +132,24 @@ class RoundPolicy
                return !$round->locked && ($user->isRunner($round->tournament) || $user->isTournamentAdmin($round->tournament));
        }
 
+       /**
+        * Determine whether the user can get the seed for this round.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\Round  $round
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function getSeed(User $user = null, Round $round)
+       {
+               if ($round->locked) {
+                       return true;
+               }
+               if (!$round->tournament->require_auth) {
+                       return true;
+               }
+               return !!$user;
+       }
+
        /**
         * Determine whether the user can lock this round.
         *
diff --git a/database/migrations/2025_11_19_150210_tournament_require_auth.php b/database/migrations/2025_11_19_150210_tournament_require_auth.php
new file mode 100644 (file)
index 0000000..93afc54
--- /dev/null
@@ -0,0 +1,29 @@
+<?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->boolean('require_auth')->default(false);
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        */
+       public function down(): void
+       {
+               Schema::table('tournaments', function (Blueprint $table) {
+                       $table->dropColumn('require_auth');
+               });
+       }
+
+};
index 35ecd1af0391b71d113678714c2a7d2ce33d4a4a..e5dbb9d23ef6a36fee977534fa181a388b85b1c3 100644 (file)
@@ -60,6 +60,7 @@ Icon.CHART = makePreset('ChartIcon', 'chart-line');
 Icon.CROSSHAIRS = makePreset('CrosshairsIcon', 'crosshairs');
 Icon.DELETE = makePreset('DeleteIcon', 'user-xmark');
 Icon.DISCORD = makePreset('DiscordIcon', ['fab', 'discord']);
+Icon.DOWNLOAD = makePreset('DownloadIcon', 'download');
 Icon.EDIT = makePreset('EditIcon', 'edit');
 Icon.ERROR = makePreset('ErrorIcon', 'triangle-exclamation');
 Icon.FILTER = makePreset('FilterIcon', 'filter');
index 17dd744e706dae0bfc035e1d15b9166908788050..eb875df1b0fa4adaec8301e732c0d565e8dd52b1 100644 (file)
@@ -69,6 +69,7 @@ const getEntryDescription = (entry, t) => {
                case 'round.create':
                case 'round.delete':
                case 'round.edit':
+               case 'round.getseed':
                case 'round.lock':
                case 'round.seed':
                case 'round.unlock':
@@ -102,6 +103,8 @@ const getEntryIcon = entry => {
                        return <Icon.ADD />;
                case 'round.delete':
                        return <Icon.REMOVE />;
+               case 'round.getseed':
+                       return <Icon.DOWNLOAD />;
                case 'round.lock':
                case 'tournament.close':
                case 'tournament.lock':
index 59bc3baaa15b2d18a6fcb16b5c5e84bf2492fec3..18a3959d80f17be81e29a619ab384b8fae10323e 100644 (file)
@@ -4,7 +4,7 @@ import { Button } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
 import SeedDialog from './SeedDialog';
-import { maySetSeed } from '../../helpers/permissions';
+import { mayGetSeed, maySetSeed } from '../../helpers/permissions';
 import { useUser } from '../../hooks/user';
 
 const SeedButton = ({ round, tournament }) => {
@@ -13,9 +13,12 @@ const SeedButton = ({ round, tournament }) => {
        const { t } = useTranslation();
        const { user } = useUser();
 
-       if (round.seed) {
+       if (round.has_seed) {
+               if (!mayGetSeed(user, tournament, round)) {
+                       return t('rounds.loginForSeed');
+               }
                return <>
-                       <Button href={round.seed} target="_blank" variant="primary">
+                       <Button href={`/rounds/${round.id}/get-seed`} target="_blank" variant="primary">
                                {t('rounds.seed')}
                        </Button>
                        {round.spoiler ?
@@ -47,6 +50,8 @@ const SeedButton = ({ round, tournament }) => {
 
 SeedButton.propTypes = {
        round: PropTypes.shape({
+               has_seed: PropTypes.bool,
+               id: PropTypes.number,
                seed: PropTypes.string,
                spoiler: PropTypes.string,
        }),
index df61b3051dec3992346f7c4e66df7cbabef67a7f..6a4ca0de141a5c08f03838bba2b3bf2b4175e1ca 100644 (file)
@@ -170,6 +170,9 @@ export const mayEditRound = (user, tournament) =>
 export const mayLockRound = (user, tournament) =>
        !tournament.locked && isTournamentAdmin(user, tournament);
 
+export const mayGetSeed = (user, tournament, round) =>
+       round.locked || !tournament.require_auth || !!user;
+
 export const maySetSeed = (user, tournament, round) =>
        !round.locked &&
                (isRunner(user, tournament) || isTournamentAdmin(user, tournament));
index 738e0a34f1ed5a5f28fdf98712fcf398053a662c..461aa50eebb17fef5b703ad232a6d5c2b8e123c9 100644 (file)
@@ -564,6 +564,7 @@ export default {
                                        create: 'Runde #{{number}} hinzugefügt',
                                        delete: 'Runde #{{number}} gelöscht',
                                        edit: 'Runde #{{number}} bearbeitet',
+                                       getseed: 'Seed für Runde #{{number}} bezogen',
                                        lock: 'Runde #{{number}} gesperrt',
                                        seed: 'Seed für Runde #{{number}} eingetragen',
                                        unlock: 'Runde #{{number}} entsperrt',
@@ -627,6 +628,7 @@ export default {
                        lockError: 'Fehler beim Sperren',
                        lockIncompleteWarning: 'Achtung: Noch nicht alle Runner haben ihr Ergebnis für diese Runde eingereicht!',
                        lockSuccess: 'Runde gesperrt',
+                       loginForSeed: 'Seed verfügbar nach Login',
                        rolled_by: 'Gerollt von',
                        rolledBy: 'Gerollt von {{name}}',
                        seed: 'Seed',
index 13c41c78d80ea517f49bf37059ac42a8281737b3..0cba64a497ff1a74db2041e82fe8697c97a7bd3f 100644 (file)
@@ -564,6 +564,7 @@ export default {
                                        create: 'Added round #{{number}}',
                                        delete: 'Deleted round #{{number}}',
                                        edit: 'Edited round #{{number}}',
+                                       getseed: 'Got seed for round #{{number}}',
                                        lock: 'Round #{{number}} locked',
                                        seed: 'Set seed for round #{{number}}',
                                        unlock: 'Round #{{number}} unlocked',
@@ -627,6 +628,7 @@ export default {
                        lockError: 'Error locking round',
                        lockIncompleteWarning: 'Warning: Not all runners have submitted their results for this round yet!',
                        lockSuccess: 'Round locked',
+                       loginForSeed: 'Seed available after login',
                        rolled_by: 'Rolled by',
                        rolledBy: 'Rolled by {{name}}',
                        seed: 'Seed',
index 0e01006ceb9c52f67f0458486376d5e8a6eb6bc2..da56bbbafbbc65f0b709e868dc40d692652a620d 100644 (file)
@@ -36,6 +36,8 @@ Route::get('/modes/{name}', function ($name) {
        return app()->call('App\Http\Controllers\TechniqueController@web', ['type' => 'mode', 'name' => $name]);
 });
 
+Route::get('/rounds/{round}/get-seed', 'App\Http\Controllers\RoundController@getSeed');
+
 Route::get('/rulesets/{name}', function ($name) {
        return app()->call('App\Http\Controllers\TechniqueController@web', ['type' => 'ruleset', 'name' => $name]);
 });