return $tournament->toJson();
}
+ public function selfAssignGroups(Request $request, Tournament $tournament) {
+ $this->authorize('selfAssignGroups', $tournament);
+ $user = $request->user();
+
+ $existing = GroupAssignment::query()
+ ->whereBelongsTo($tournament)
+ ->whereBelongsTo($user)
+ ->get()
+ ->pluck('round_number');
+
+ $round_numbers = $tournament->rounds->pluck('number')->unique();
+
+ $picks = [];
+ foreach ($round_numbers as $number) {
+ if (!$existing->contains($number)) {
+ $group = $tournament->pickGroup($number, $user);
+ $picks[] = [
+ 'number' => $number,
+ 'group' => $group,
+ ];
+ }
+ }
+
+ if (!empty($picks)) {
+ foreach ($picks as $pick) {
+ GroupAssignment::create([
+ 'tournament_id' => $tournament->id,
+ 'user_id' => $user->id,
+ 'round_number' => $pick['number'],
+ 'group' => $pick['group'],
+ ]);
+ Protocol::groupAssignment($tournament, $user, $picks, $user);
+ }
+ }
+
+ return GroupAssignment::query()
+ ->whereBelongsTo($tournament)
+ ->whereBelongsTo($$user)
+ ->get()
+ ->toJson();
+ }
+
public function web(Request $request, Tournament $tournament) {
$view = view('app')
->with('title', $tournament->getTranslatedTitle())
ProtocolAdded::dispatch($protocol);
}
+ public static function groupAssignment(Tournament $tournament, User $assignee, $picks, User $user) {
+ $protocol = static::create([
+ 'tournament_id' => $tournament->id,
+ 'user_id' => $user->id,
+ 'type' => 'group.assign',
+ 'details' => [
+ 'tournament' => static::tournamentMemo($tournament),
+ 'assignee' => static::userMemo($assignee),
+ 'picks' => $picks,
+ ],
+ ]);
+ ProtocolAdded::dispatch($protocol);
+ }
+
public static function resultCommented(Tournament $tournament, Result $result, User $user) {
$protocol = static::create([
'tournament_id' => $tournament->id,
}
}
+ public function pickGroup($number, User $user) {
+ $available_rounds = $this->round()->where('number', '=', $number)->get();
+ $assigned_groups = $this->group_assignments()->where('round_number', '=', $number)->get();
+ $weights = array();
+ foreach ($available_rounds as $round) {
+ $weights[$round->group] = $assigned_groups->count() + 1;
+ }
+ foreach ($assigned_groups as $assignment) {
+ --$weights[$assignment->group];
+ }
+ $rand = random_int(1, array_sum($weights));
+ foreach ($weights as $group => $weight) {
+ $rand -= $weight;
+ if ($rand <= 0) {
+ return $group;
+ }
+ }
+ return 'A';
+ }
+
public function applications() {
return $this->hasMany(Application::class);
return $this->belongsTo(Technique::class);
}
+ public function group_assignments() {
+ return $this->hasMany(GroupAssignment::class);
+ }
+
public function participants() {
return $this->hasMany(Participant::class);
}
return $user->isTournamentCrew($tournament);
}
+ /**
+ * Determine whether the user self assign groups within the tournament.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Tournament $tournament
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function selfAssignGroups(User $user, Tournament $tournament)
+ {
+ return !!$user;
+ }
+
}
import { useTranslation } from 'react-i18next';
import ApplyButton from './ApplyButton';
+import GroupInterface from './GroupInterface';
import Scoreboard from './Scoreboard';
import ScoreChartButton from './ScoreChartButton';
import SettingsButton from './SettingsButton';
import {
getTournamentAdmins,
getTournamentMonitors,
+ hasAssignedGroups,
hasRunners,
hasScoreboard,
hasTournamentAdmins,
</div>
</Col>
<Col lg={{ order: 1, span: 8 }} xl={{ order: 1, span: 9 }}>
+ {hasAssignedGroups(tournament) ?
+ <GroupInterface
+ selfAssign={actions.selfAssignGroups}
+ tournament={tournament}
+ />
+ : null}
<div className="d-flex align-items-center justify-content-between">
<h2>{t('rounds.heading')}</h2>
{actions.addRound && mayAddRounds(user, tournament) ?
addRound: PropTypes.func,
editContent: PropTypes.func,
moreRounds: PropTypes.func,
+ selfAssignGroups: PropTypes.func,
}).isRequired,
tournament: PropTypes.shape({
description: PropTypes.shape({
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import { missingGroupAssignment } from '../../helpers/Tournament';
+import { useUser } from '../../hooks/user';
+
+const GroupInterface = ({ selfAssign, tournament }) => {
+ const { t } = useTranslation();
+ const { user } = useUser();
+
+ if (!user) {
+ return <div><p>{t('groups.loginRequired')}</p></div>
+ }
+
+ if (missingGroupAssignment(tournament, user)) {
+ return <div>
+ <p>{t('groups.missingAssignments')}</p>
+ <Button onClick={selfAssign}>
+ {t('groups.selfAssignButton')}
+ </Button>
+ </div>
+ }
+
+ return <div>
+ Groups here
+ </div>;
+};
+
+GroupInterface.propTypes = {
+ selfAssign: PropTypes.func,
+ tournament: PropTypes.shape({
+ }),
+};
+
+export default GroupInterface;
return last_round && last_round.number > 1;
};
+export const hasAssignedGroups = tournament => (tournament?.type === 'open-grouped-async');
+
export const hasScoreboard = tournament => !!(tournament && tournament.type === 'signup-async');
export const hasSignup = tournament => !!(tournament && tournament.type === 'signup-async');
return getTournamentMonitors(tournament).length > 0;
};
+const unique = (value, index, array) => array.indexOf(value) === index;
+
+export const missingGroupAssignment = (tournament, user) => {
+ if (!user) return true;
+ if (!tournament?.group_assignments?.length) return false;
+ if (!tournament.rounds?.length) return false;
+ const gas = tournament.group_assignments;
+ const rns = tournament.rounds.map(r => r.number).filter(unique);
+ for (let i = 0; i < rns.length; ++i) {
+ if (!gas.find(ga => ga.round_number === rns[i])) {
+ return true;
+ }
+ }
+ return false;
+}
+
export const patchApplication = (tournament, application) => {
if (!tournament) return tournament;
if (!tournament.applications || !tournament.applications.length) {
uploadError: 'Fehler beim Hochladen',
uploading: 'Am Hochladen...',
},
+ groups: {
+ loginRequired: 'Dieses Turnier nutzt Gruppenzuweisung. Bitte melde dich an, um deine Seeds zu laden.',
+ missingAssignments: 'Dieses Turnier nutzt Gruppenzuweisung. Falls du teilnehmen möchtest, hol dir bitter hier deine Zuweisungen ab.',
+ selfAssignButton: 'Gruppen zuweisen',
+ selfAssignError: 'Fehler beim Zuweisen',
+ selfAssignSuccess: 'Gruppen zugewiesen',
+ },
icon: {
AddIcon: 'Hinzufügen',
AllowedIcon: 'Erlaubt',
uploadError: 'Error uploading',
uploading: 'Uploading...',
},
+ groups: {
+ loginRequired: 'This tournament uses assigned groups. Please sign in to obtain your seeds.',
+ missingAssignments: 'This tournament uses assigned groups. If you want to participate, please grab your assignments here.',
+ selfAssignButton: 'Assign groups',
+ selfAssignError: 'Error assigning groups',
+ selfAssignSuccess: 'Groups assigned',
+ },
icon: {
AddIcon: 'Add',
AllowedIcon: 'Allowed',
}
}, [tournament && tournament.description_id]);
+ const selfAssignGroups = React.useCallback(async () => {
+ try {
+ const response = await axios.post(`/api/tournaments/${id}/self-assign-groups`);
+ toastr.success(t('groups.selfAssignSuccess'));
+ setTournament(tournament => ({
+ ...tournament,
+ group_assignments: response.data,
+ }));
+ } catch (e) {
+ toastr.error(t('groups.selfAssignError', e));
+ }
+ }, [id, t]);
+
const actions = React.useMemo(() => ({
addRound,
editContent: mayEditContent(user) ? content => {
setShowContentDialog(true);
} : null,
moreRounds: canLoadMoreRounds(tournament) ? moreRounds : null,
- }), [addRound, moreRounds, tournament, user]);
+ selfAssignGroups,
+ }), [addRound, moreRounds, selfAssignGroups, tournament, user]);
useEffect(() => {
const cb = (e) => {
Route::post('tournaments/{tournament}/lock', 'App\Http\Controllers\TournamentController@lock');
Route::post('tournaments/{tournament}/open', 'App\Http\Controllers\TournamentController@open');
Route::post('tournaments/{tournament}/settings', 'App\Http\Controllers\TournamentController@settings');
+Route::post('tournaments/{tournament}/self-assign-groups', 'App\Http\Controllers\TournamentController@selfAssignGroups');
Route::post('tournaments/{tournament}/unlock', 'App\Http\Controllers\TournamentController@unlock');
Route::get('users', 'App\Http\Controllers\UserController@search');