1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Http\Controllers;
7
8use App\Exceptions\InvariantException;
9use App\Models\Contest;
10use App\Models\ContestEntry;
11use App\Models\UserContestEntry;
12use App\Transformers\ContestTransformer;
13use Auth;
14use Ds\Set;
15use Illuminate\Support\Facades\DB;
16use Request;
17
18class ContestEntriesController extends Controller
19{
20 public function judgeResults($id)
21 {
22 $entry = ContestEntry::with('contest')->findOrFail($id);
23
24 abort_if(!$entry->contest->isJudged() || !$entry->contest->show_votes, 404);
25
26 $entry->load([
27 'contest.entries',
28 'contest.scoringCategories',
29 'judgeVotes.scores',
30 'judgeVotes.user',
31 'user',
32 ])->loadSum('scores', 'value');
33
34 $contest = $entry->contest
35 ->loadCount('judges')
36 ->loadSum('scoringCategories', 'max_value');
37
38 $contestJson = json_item(
39 $contest,
40 new ContestTransformer(),
41 [
42 'max_judging_score',
43 'max_total_score',
44 'scoring_categories',
45 ],
46 );
47
48 $entryJson = json_item($entry, 'ContestEntry', [
49 'judge_votes.scores',
50 'judge_votes.total_score',
51 'judge_votes.user',
52 'results',
53 'user',
54 ]);
55
56 $entriesJson = json_collection($entry->contest->entries, 'ContestEntry');
57
58 return ext_view('contest_entries.judge-results', [
59 'contestJson' => $contestJson,
60 'entryJson' => $entryJson,
61 'entriesJson' => $entriesJson,
62 ]);
63 }
64
65 public function judgeVote($id)
66 {
67 $entry = ContestEntry::with('contest.scoringCategories')->findOrFail($id);
68
69 priv_check('ContestJudge', $entry->contest)->ensureCan();
70
71 $params = get_params(request()->all(), null, [
72 'scores:array',
73 'comment',
74 ], ['null_missing' => true]);
75
76 $scoresByCategoryId = collect($params['scores'])
77 ->keyBy('contest_scoring_category_id');
78
79 $expectedCategoryIds = new Set($entry->contest->scoringCategories->pluck('id'));
80 $givenCategoryIds = new Set($scoresByCategoryId->keys());
81
82 if ($expectedCategoryIds->diff($givenCategoryIds)->count() > 0) {
83 throw new InvariantException(osu_trans('contest.judge.validation.missing_score'));
84 }
85
86 DB::transaction(function () use ($entry, $params, $scoresByCategoryId) {
87 $vote = $entry->judgeVotes()->firstOrNew(['user_id' => Auth::user()->getKey()]);
88 $vote->fill(['comment' => $params['comment']])->save();
89
90 foreach ($entry->contest->scoringCategories as $category) {
91 $score = $scoresByCategoryId[$category->getKey()];
92 $value = \Number::clamp($score['value'], 0, $category->max_value);
93
94 $vote->scores()->firstOrNew([
95 'contest_judge_vote_id' => $vote->getKey(),
96 'contest_scoring_category_id' => $category->getKey(),
97 ])->fill(['value' => $value])->save();
98 }
99 });
100
101 $updatedEntry = $entry->refresh()->load('judgeVotes.scores');
102
103 return json_item($updatedEntry, 'ContestEntry', ['current_user_judge_vote.scores']);
104 }
105
106 public function vote($id)
107 {
108 $user = Auth::user();
109 $entry = ContestEntry::findOrFail($id);
110 $contest = Contest::with('entries')->with('entries.contest')->findOrFail($entry->contest_id);
111
112 if ($contest->isJudged()) {
113 throw new InvariantException(osu_trans('contest.judge.validation.contest_vote_judged'));
114 }
115
116 priv_check('ContestVote', $contest)->ensureCan();
117
118 $contest->vote($user, $entry);
119
120 return $contest->defaultJson($user);
121 }
122
123 public function store()
124 {
125 if (Request::hasFile('entry') !== true) {
126 abort(422, 'No file uploaded');
127 }
128
129 $user = Auth::user();
130 $contest = Contest::findOrFail(Request::input('contest_id'));
131 $file = Request::file('entry');
132
133 priv_check('ContestEntryStore', $contest)->ensureCan();
134
135 $allowedExtensions = [];
136 $maxFilesize = 0;
137 switch ($contest->type) {
138 case 'art':
139 $allowedExtensions[] = 'jpg';
140 $allowedExtensions[] = 'jpeg';
141 $allowedExtensions[] = 'png';
142 $maxFilesize = 8 * 1024 * 1024;
143 break;
144 case 'beatmap':
145 $allowedExtensions[] = 'osu';
146 $allowedExtensions[] = 'osz';
147 $maxFilesize = 32 * 1024 * 1024;
148 break;
149 case 'music':
150 $allowedExtensions[] = 'mp3';
151 $maxFilesize = 16 * 1024 * 1024;
152 break;
153 }
154
155 if (!in_array(strtolower($file->getClientOriginalExtension()), $allowedExtensions, true)) {
156 abort(
157 422,
158 'Files for this contest must have one of the following extensions: '.implode(', ', $allowedExtensions)
159 );
160 }
161
162 if ($file->getSize() > $maxFilesize) {
163 abort(413, 'File exceeds max size');
164 }
165
166 if ($contest->type === 'art' && !is_null($contest->getForcedWidth()) && !is_null($contest->getForcedHeight())) {
167 if (empty($file->getContent())) {
168 abort(422, 'File must not be empty');
169 }
170
171 [$width, $height] = read_image_properties_from_string($file->getContent()) ?? [null, null];
172
173 if ($contest->getForcedWidth() !== $width || $contest->getForcedHeight() !== $height) {
174 abort(
175 422,
176 "Images for this contest must be {$contest->getForcedWidth()}x{$contest->getForcedHeight()}"
177 );
178 }
179 }
180
181 UserContestEntry::upload($file, $user, $contest);
182
183 return $contest->userEntries($user);
184 }
185
186 public function destroy($id)
187 {
188 $user = Auth::user();
189 $entry = UserContestEntry::where(['user_id' => $user->user_id])->findOrFail($id);
190 $contest = Contest::findOrFail($entry->contest_id);
191
192 priv_check('ContestEntryDestroy', $entry)->ensureCan();
193
194 $entry->delete();
195
196 return $contest->userEntries($user);
197 }
198}