Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions app/Http/Controllers/JudgeController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

namespace App\Http\Controllers;

use App\Models\Answer;
use App\Models\Contest;
use App\Models\ContestLog;
use App\Models\Run;
use App\Models\Score;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

/**
* Manual judging screen for the "judge" and "admin" roles. BOCA's judge/run.php
* (own runs) and judge/runchief.php (chief judge, all runs) are collapsed into
* one screen here since there's no per-judge run assignment/locking in the
* current schema -- any judge/admin can see and judge any pending run in
* their contest.
*/
class JudgeController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'role:judge,admin']);
}

public function index(): View
{
$contest = $this->resolveContest();

$pendingRuns = collect();
$judgedRuns = collect();

if ($contest) {
$runs = Run::where('contest_id', $contest->id)
->with(['problem:id,short_name,name', 'language:id,name', 'user:user_id,fullname,username', 'answer:id,name,short_name,is_accepted'])
->orderByDesc('created_at')
->get();

$pendingRuns = $runs->whereIn('status', ['pending', 'judging']);
$judgedRuns = $runs->where('status', 'judged')->take(50);
}

$answers = $contest
? Answer::where('contest_id', $contest->id)->orderBy('sort_order')->get()
: collect();

return view('judge.runs', compact('contest', 'pendingRuns', 'judgedRuns', 'answers'));
}

public function judge(Request $request, Run $run): RedirectResponse
{
$this->authorizeRunAccess($run);

if ($run->status === 'judged') {
return back()->withErrors(['answer_id' => "Run #{$run->run_number} ja foi julgada. Use a API de rejudge para reabrir antes de julgar de novo."]);
}

$validated = $request->validate([
'answer_id' => 'required|exists:answers,id',
]);

$answer = Answer::findOrFail($validated['answer_id']);
if ($answer->contest_id !== $run->contest_id) {
abort(422, 'Essa resposta nao pertence ao contest desta submissao.');
}

$run->update([
'status' => 'judged',
'answer_id' => $answer->id,
'judge_id' => auth()->id(),
'judge_site_id' => auth()->user()->site_id,
'judged_time' => $run->contest->getContestTime(),
]);

Score::updateScore($run);

ContestLog::info($run->contest_id, "Run #{$run->run_number} manually judged", [
'judge_id' => auth()->id(),
'answer_id' => $answer->id,
]);

return redirect()->route('judge.runs')->with('success', "Run #{$run->run_number} julgada como {$answer->name}.");
}

private function resolveContest(): ?Contest
{
$user = auth()->user();

if ($user->contest_id) {
return Contest::find($user->contest_id);
}

return Contest::where('is_active', true)->first();
}

/**
* A judge must only be able to judge runs in their own contest -- the
* `role:judge,admin` middleware only checks the user's type, not which
* contest the run being acted on belongs to, so without this a judge
* from contest A could judge (and thus score) a run in contest B just
* by guessing/incrementing the run id. Admins are trusted across
* contests, matching the rest of the admin surface.
*/
private function authorizeRunAccess(Run $run): void
{
$user = auth()->user();

if ($user->isAdmin()) {
return;
}

if ($user->contest_id !== $run->contest_id) {
abort(403, 'Voce nao pode julgar submissoes de outro contest.');
}
}
}
81 changes: 81 additions & 0 deletions app/Http/Controllers/StaffController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace App\Http\Controllers;

use App\Models\Contest;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

/**
* Staff task screen (BOCA's staff/task.php) -- printing/balloon-delivery
* style tasks that a staff member claims and marks done.
*/
class StaffController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'role:staff,admin']);
}

public function tasks(): View
{
$contest = $this->resolveContest();

$tasks = $contest
? Task::where('contest_id', $contest->id)
->where('status', '!=', 'deleted')
->with(['user:user_id,fullname,username', 'staff:user_id,fullname,username'])
->orderByDesc('created_at')
->get()
: collect();

return view('staff.tasks', compact('contest', 'tasks'));
}

public function complete(Task $task): RedirectResponse
{
$this->authorizeTaskAccess($task);

$task->update([
'status' => 'done',
'completed_time' => $task->contest->getContestTime(),
'staff_id' => auth()->id(),
'staff_site_id' => auth()->user()->site_id,
]);

return redirect()->route('staff.tasks')->with('success', "Tarefa #{$task->task_number} marcada como concluida.");
}

private function resolveContest(): ?Contest
{
$user = auth()->user();

if ($user->contest_id) {
return Contest::find($user->contest_id);
}

return Contest::where('is_active', true)->first();
}

/**
* A staff member must only be able to complete tasks in their own
* contest -- the `role:staff,admin` middleware only checks the user's
* type, not which contest the task belongs to, so without this a staff
* member from contest A could mark a task in contest B done just by
* guessing/incrementing the task id. Admins are trusted across
* contests, matching the rest of the admin surface.
*/
private function authorizeTaskAccess(Task $task): void
{
$user = auth()->user();

if ($user->isAdmin()) {
return;
}

if ($user->contest_id !== $task->contest_id) {
abort(403, 'Voce nao pode concluir tarefas de outro contest.');
}
}
}
1 change: 1 addition & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'admin' => \Helium\Http\Middleware\IsAdminMiddleware::class,
'role' => \App\Http\Middleware\CheckRole::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
Expand Down
97 changes: 97 additions & 0 deletions resources/views/judge/runs.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
@extends('layouts.app')

@section('title', 'Julgar Submissões')

@section('content')
<div class="space-y-6">
@if(session('success'))
<div class="px-4 py-3 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 rounded-lg text-sm">
{{ session('success') }}
</div>
@endif

<div class="bg-card rounded-lg border border-border shadow-sm">
<div class="p-6 border-b border-border">
<h2 class="text-xl font-semibold text-foreground">Pendentes de Julgamento</h2>
<p class="text-sm text-muted-foreground">{{ $contest?->name ?? 'Nenhum contest ativo' }}</p>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Run</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Time</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Problema</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Linguagem</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Julgar</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
@forelse ($pendingRuns as $run)
<tr class="hover:bg-muted/50 transition-colors">
<td class="px-4 py-3 font-mono text-sm">#{{ $run->run_number }}</td>
<td class="px-4 py-3 text-sm">{{ $run->user->fullname ?? $run->user->username }}</td>
<td class="px-4 py-3 text-sm">{{ $run->problem->short_name }} - {{ $run->problem->name }}</td>
<td class="px-4 py-3 text-sm">{{ $run->language->name }}</td>
<td class="px-4 py-3">
<form action="{{ route('judge.runs.judge', $run) }}" method="POST" class="flex items-center gap-2">
@csrf
<select name="answer_id" required class="text-sm px-2 py-1.5 bg-background border border-border rounded-lg">
<option value="">Veredito...</option>
@foreach($answers as $answer)
<option value="{{ $answer->id }}">{{ $answer->name }}</option>
@endforeach
</select>
<button type="submit" class="px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors">
Confirmar
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-4 py-12 text-center text-muted-foreground">Nenhuma submissao pendente</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>

<div class="bg-card rounded-lg border border-border shadow-sm">
<div class="p-6 border-b border-border">
<h2 class="text-xl font-semibold text-foreground">Julgadas Recentemente</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-muted/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Run</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Time</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Problema</th>
<th class="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Veredito</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
@forelse ($judgedRuns as $run)
<tr class="hover:bg-muted/50 transition-colors">
<td class="px-4 py-3 font-mono text-sm">#{{ $run->run_number }}</td>
<td class="px-4 py-3 text-sm">{{ $run->user->fullname ?? $run->user->username }}</td>
<td class="px-4 py-3 text-sm">{{ $run->problem->short_name }} - {{ $run->problem->name }}</td>
<td class="px-4 py-3">
<span class="px-2 py-1 text-xs font-medium rounded {{ $run->answer?->is_accepted ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' }}">
{{ $run->answer->short_name ?? '-' }}
</span>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-12 text-center text-muted-foreground">Nenhuma submissao julgada ainda</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
@endsection
36 changes: 36 additions & 0 deletions resources/views/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,41 @@
</a>
</div>

@if(Auth::check() && (Auth::user()->isJudge() || Auth::user()->isAdmin()))
<!-- Judge Section -->
<div class="mt-6 px-3">
<h3 class="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Julgamento
</h3>
<div class="mt-2 space-y-1">
<a href="/judge/runs" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors {{ Request::is('judge/runs*') ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground' }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
Julgar Submissões
</a>
</div>
</div>
@endif

@if(Auth::check() && (Auth::user()->isStaff() || Auth::user()->isAdmin()))
<!-- Staff Section -->
<div class="mt-6 px-3">
<h3 class="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Staff
</h3>
<div class="mt-2 space-y-1">
<a href="/staff/tasks" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors {{ Request::is('staff/tasks*') ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground' }}">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
Tarefas
</a>
</div>
</div>
@endif

@if(Auth::check() && Auth::user()->isAdmin())
<!-- Admin Section -->
<div class="mt-6 px-3">
<h3 class="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Expand Down Expand Up @@ -131,6 +166,7 @@
</a>
</div>
</div>
@endif
</nav>
</div>
</aside>
Expand Down
Loading