Dépôt de fichier en plus du texte libre pour les devoirs maison, stocké sur Hetzner Object Storage (jamais sur le disque du serveur), masqué côté élève tant que le bucket n'est pas configuré. Purge RGPD étendue pour supprimer aussi les fichiers d'Object Storage. Ansible lit les identifiants depuis un fichier que seul Adam peut créer sur le serveur (runbook docs/runbooks/stockage-s3.md) ; le bloc B du CLAUDE.md est maintenant complet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
3.1 KiB
PHP
80 lines
3.1 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Models\Devoir;
|
||
use App\Models\Eleve;
|
||
use App\Models\RenduDevoir;
|
||
use App\Services\Stockage\StockageTravauxEleves;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Illuminate\View\View;
|
||
|
||
/**
|
||
* Côté élève des devoirs maison (chantier « vrai cockpit », étape 3) :
|
||
* rendu en texte libre, une seule fois par devoir, avant l'échéance.
|
||
*/
|
||
class DevoirEleveController extends Controller
|
||
{
|
||
public function show(Devoir $devoir): View
|
||
{
|
||
$eleve = $this->verifierAppartenance($devoir);
|
||
|
||
return view('devoirs.eleve', [
|
||
'devoir' => $devoir,
|
||
'rendu' => RenduDevoir::where('devoir_id', $devoir->id)->where('eleve_id', $eleve->id)->first(),
|
||
'depotFichierDisponible' => StockageTravauxEleves::disponible(),
|
||
]);
|
||
}
|
||
|
||
public function rendre(Request $request, Devoir $devoir): RedirectResponse
|
||
{
|
||
$eleve = $this->verifierAppartenance($devoir);
|
||
abort_if($devoir->echeanceDepassee(), 409, 'L’échéance est dépassée.');
|
||
|
||
$donnees = $request->validate([
|
||
'reponse' => ['nullable', 'required_without:fichier', 'string', 'max:10000'],
|
||
'fichier' => [
|
||
'nullable', 'required_without:reponse', 'file',
|
||
'max:'.(int) (StockageTravauxEleves::TAILLE_MAX_OCTETS / 1024),
|
||
'mimes:'.implode(',', StockageTravauxEleves::EXTENSIONS_ACCEPTEES),
|
||
],
|
||
]);
|
||
|
||
if ($request->hasFile('fichier') && ! StockageTravauxEleves::disponible()) {
|
||
abort(503, 'Le dépôt de fichier n’est pas disponible pour le moment.');
|
||
}
|
||
|
||
$ancienRendu = RenduDevoir::where('devoir_id', $devoir->id)->where('eleve_id', $eleve->id)->first();
|
||
|
||
$valeurs = ['reponse' => $donnees['reponse'] ?? null, 'rendu_a' => now()];
|
||
|
||
// Pas de nouveau fichier : celui déjà rendu (s'il existe) reste en
|
||
// place, updateOrCreate ne touche pas aux colonnes absentes de $valeurs.
|
||
if ($request->hasFile('fichier')) {
|
||
$fichier = StockageTravauxEleves::stocker($request->file('fichier'), (string) $devoir->id, (string) $eleve->id);
|
||
$valeurs['fichier_chemin'] = $fichier['chemin'];
|
||
$valeurs['fichier_nom_original'] = $fichier['nom_original'];
|
||
$valeurs['fichier_taille_octets'] = $fichier['taille_octets'];
|
||
}
|
||
|
||
RenduDevoir::updateOrCreate(['devoir_id' => $devoir->id, 'eleve_id' => $eleve->id], $valeurs);
|
||
|
||
// Un nouveau fichier remplace l'ancien : celui-ci n'a plus de raison
|
||
// d'exister sur Object Storage.
|
||
if ($request->hasFile('fichier') && $ancienRendu?->fichier_chemin) {
|
||
StockageTravauxEleves::supprimer($ancienRendu->fichier_chemin);
|
||
}
|
||
|
||
return redirect()->route('devoirs.eleve.show', $devoir)->with('rendu_envoye', true);
|
||
}
|
||
|
||
private function verifierAppartenance(Devoir $devoir): Eleve
|
||
{
|
||
$eleve = Auth::guard('eleve')->user();
|
||
abort_if($devoir->classe_id !== $eleve->classe_id, 403);
|
||
|
||
return $eleve;
|
||
}
|
||
}
|