ajoute le stockage S3 des devoirs maison (bloc B, dernier chantier)
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>
This commit is contained in:
parent
79d4a06fdf
commit
6ea7a1cc16
16 changed files with 1022 additions and 59 deletions
|
|
@ -3,7 +3,9 @@
|
|||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\RenduDevoir;
|
||||
use App\Services\CacheTableauDeBordProf;
|
||||
use App\Services\Stockage\StockageTravauxEleves;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -44,6 +46,13 @@ class PurgerClassesArchivees extends Command
|
|||
continue;
|
||||
}
|
||||
|
||||
// Les fichiers d'Object Storage ne sont pas liés par une clé
|
||||
// étrangère : à supprimer nous-mêmes avant la cascade en base.
|
||||
RenduDevoir::whereHas('devoir', fn ($q) => $q->where('classe_id', $classe->id))
|
||||
->whereNotNull('fichier_chemin')
|
||||
->pluck('fichier_chemin')
|
||||
->each(fn (string $chemin) => StockageTravauxEleves::supprimer($chemin));
|
||||
|
||||
DB::transaction(fn () => $classe->delete());
|
||||
CacheTableauDeBordProf::vider($classe->user_id);
|
||||
Log::info('Purge RGPD : classe supprimée', [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
|||
use App\Models\Classe;
|
||||
use App\Models\Devoir;
|
||||
use App\Models\RenduDevoir;
|
||||
use App\Services\Stockage\StockageTravauxEleves;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
|
@ -64,6 +65,16 @@ class DevoirController extends Controller
|
|||
return redirect()->route('mes-classes.devoirs.show', [$classe, $devoir]);
|
||||
}
|
||||
|
||||
public function telechargerRendu(Classe $classe, Devoir $devoir, RenduDevoir $rendu): RedirectResponse
|
||||
{
|
||||
$this->verifierProprietaire($classe);
|
||||
$this->verifierAppartenance($classe, $devoir);
|
||||
abort_if($rendu->devoir_id !== $devoir->id, 404);
|
||||
abort_if($rendu->fichier_chemin === null, 404);
|
||||
|
||||
return redirect()->away(StockageTravauxEleves::urlTemporaire($rendu->fichier_chemin));
|
||||
}
|
||||
|
||||
public function destroy(Classe $classe, Devoir $devoir): RedirectResponse
|
||||
{
|
||||
$this->verifierProprietaire($classe);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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;
|
||||
|
|
@ -23,6 +24,7 @@ class DevoirEleveController extends Controller
|
|||
return view('devoirs.eleve', [
|
||||
'devoir' => $devoir,
|
||||
'rendu' => RenduDevoir::where('devoir_id', $devoir->id)->where('eleve_id', $eleve->id)->first(),
|
||||
'depotFichierDisponible' => StockageTravauxEleves::disponible(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -31,12 +33,39 @@ class DevoirEleveController extends Controller
|
|||
$eleve = $this->verifierAppartenance($devoir);
|
||||
abort_if($devoir->echeanceDepassee(), 409, 'L’échéance est dépassée.');
|
||||
|
||||
$donnees = $request->validate(['reponse' => ['required', 'string', 'max:10000']]);
|
||||
$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),
|
||||
],
|
||||
]);
|
||||
|
||||
RenduDevoir::updateOrCreate(
|
||||
['devoir_id' => $devoir->id, 'eleve_id' => $eleve->id],
|
||||
['reponse' => $donnees['reponse'], 'rendu_a' => now()]
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class RenduDevoir extends Model
|
|||
{
|
||||
protected $table = 'rendus_devoirs';
|
||||
|
||||
protected $fillable = ['devoir_id', 'eleve_id', 'reponse', 'note', 'rendu_a'];
|
||||
protected $fillable = ['devoir_id', 'eleve_id', 'reponse', 'fichier_chemin', 'fichier_nom_original', 'fichier_taille_octets', 'note', 'rendu_a'];
|
||||
|
||||
protected $casts = [
|
||||
'rendu_a' => 'datetime',
|
||||
|
|
@ -27,4 +27,9 @@ class RenduDevoir extends Model
|
|||
{
|
||||
return $this->belongsTo(Eleve::class);
|
||||
}
|
||||
|
||||
public function aUnFichier(): bool
|
||||
{
|
||||
return $this->fichier_chemin !== null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
58
portail/app/Services/Stockage/StockageTravauxEleves.php
Normal file
58
portail/app/Services/Stockage/StockageTravauxEleves.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Stockage;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Stockage des rendus de devoirs maison sur Object Storage (bloc B,
|
||||
* Hetzner, 2026-10-14) : jamais sur le disque du serveur. Le bouton de
|
||||
* dépôt de fichier n'apparaît que si le disque est vraiment configuré
|
||||
* (CLAUDE.md : pas de fonction qui a l'air de marcher sans vraiment
|
||||
* marcher) — tant qu'Adam n'a pas créé le bucket, seul le texte libre
|
||||
* reste proposé.
|
||||
*/
|
||||
class StockageTravauxEleves
|
||||
{
|
||||
public const TAILLE_MAX_OCTETS = 10 * 1024 * 1024; // 10 Mo
|
||||
|
||||
public const EXTENSIONS_ACCEPTEES = ['pdf', 'doc', 'docx', 'odt', 'txt', 'png', 'jpg', 'jpeg', 'zip'];
|
||||
|
||||
public static function disponible(): bool
|
||||
{
|
||||
return filled(config('filesystems.disks.s3.bucket'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{chemin: string, nom_original: string, taille_octets: int}
|
||||
*/
|
||||
public static function stocker(UploadedFile $fichier, string $devoirId, string $eleveId): array
|
||||
{
|
||||
$chemin = $fichier->storeAs(
|
||||
"devoirs/{$devoirId}/{$eleveId}",
|
||||
now()->format('Ymd-His').'-'.Str::random(8).'.'.$fichier->getClientOriginalExtension(),
|
||||
's3'
|
||||
);
|
||||
|
||||
return [
|
||||
'chemin' => $chemin,
|
||||
'nom_original' => $fichier->getClientOriginalName(),
|
||||
'taille_octets' => $fichier->getSize(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function supprimer(?string $chemin): void
|
||||
{
|
||||
if ($chemin !== null) {
|
||||
Storage::disk('s3')->delete($chemin);
|
||||
}
|
||||
}
|
||||
|
||||
/** Lien valable 5 minutes, jamais le fichier servi en clair par le portail. */
|
||||
public static function urlTemporaire(string $chemin): string
|
||||
{
|
||||
return Storage::disk('s3')->temporaryUrl($chemin, now()->addMinutes(5));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,12 @@
|
|||
"require": {
|
||||
"php": "^8.3",
|
||||
"bacon/bacon-qr-code": "^3.1",
|
||||
"doctrine/dbal": "^4.5",
|
||||
"laravel/framework": "^13.17",
|
||||
"laravel/horizon": "^5.50",
|
||||
"laravel/sanctum": "^4.3",
|
||||
"laravel/tinker": "^3.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"symfony/yaml": "^7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
|
|
|
|||
595
portail/composer.lock
generated
595
portail/composer.lock
generated
|
|
@ -4,8 +4,159 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4f63b39390b44505487b3c5999569f85",
|
||||
"content-hash": "6f5b54180f0d3c00c4708c345a8ac918",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
"version": "v1.2.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/awslabs/aws-crt-php.git",
|
||||
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
|
||||
"reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
|
||||
"yoast/phpunit-polyfills": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "AWS SDK Common Runtime Team",
|
||||
"email": "aws-sdk-common-runtime@amazon.com"
|
||||
}
|
||||
],
|
||||
"description": "AWS Common Runtime for PHP",
|
||||
"homepage": "https://github.com/awslabs/aws-crt-php",
|
||||
"keywords": [
|
||||
"amazon",
|
||||
"aws",
|
||||
"crt",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/awslabs/aws-crt-php/issues",
|
||||
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
|
||||
},
|
||||
"time": "2024-10-18T22:15:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.401.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "4cdda6c2d836fa91a8177535530952c96a582f57"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4cdda6c2d836fa91a8177535530952c96a582f57",
|
||||
"reference": "4cdda6c2d836fa91a8177535530952c96a582f57",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"aws/aws-crt-php": "^1.2.3",
|
||||
"ext-json": "*",
|
||||
"ext-pcre": "*",
|
||||
"ext-simplexml": "*",
|
||||
"guzzlehttp/guzzle": "^7.8.2 || ^8.0",
|
||||
"guzzlehttp/promises": "^2.0.3 || ^3.0",
|
||||
"guzzlehttp/psr7": "^2.6.3 || ^3.0",
|
||||
"mtdowling/jmespath.php": "^2.9.1",
|
||||
"php": ">=8.1",
|
||||
"psr/http-message": "^1.0 || ^2.0",
|
||||
"symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"andrewsville/php-token-reflection": "^1.4",
|
||||
"aws/aws-php-sns-message-validator": "~1.0",
|
||||
"behat/behat": "~3.0",
|
||||
"composer/composer": "^2.7.8",
|
||||
"dms/phpunit-arraysubset-asserts": "^v0.5.0",
|
||||
"doctrine/cache": "~1.4",
|
||||
"ext-dom": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-sockets": "*",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"psr/cache": "^2.0 || ^3.0",
|
||||
"psr/simple-cache": "^2.0 || ^3.0",
|
||||
"sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
|
||||
"yoast/phpunit-polyfills": "^2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
|
||||
"doctrine/cache": "To use the DoctrineCacheAdapter",
|
||||
"ext-curl": "To send requests using cURL",
|
||||
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
|
||||
"ext-pcntl": "To use client-side monitoring",
|
||||
"ext-sockets": "To use client-side monitoring"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Aws\\": "src/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"src/data/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Amazon Web Services",
|
||||
"homepage": "https://aws.amazon.com"
|
||||
}
|
||||
],
|
||||
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
|
||||
"homepage": "https://aws.amazon.com/sdk-for-php",
|
||||
"keywords": [
|
||||
"amazon",
|
||||
"aws",
|
||||
"cloud",
|
||||
"dynamodb",
|
||||
"ec2",
|
||||
"glacier",
|
||||
"s3",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.401.1"
|
||||
},
|
||||
"time": "2026-10-13T18:14:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"version": "v3.1.1",
|
||||
|
|
@ -320,6 +471,160 @@
|
|||
},
|
||||
"time": "2024-07-08T12:26:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/dbal",
|
||||
"version": "4.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/dbal.git",
|
||||
"reference": "c435cd7ea33e17d2a89288caf994cf3d4549b879"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/dbal/zipball/c435cd7ea33e17d2a89288caf994cf3d4549b879",
|
||||
"reference": "c435cd7ea33e17d2a89288caf994cf3d4549b879",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/deprecations": "^1.1.5",
|
||||
"php": "^8.2",
|
||||
"psr/cache": "^1|^2|^3",
|
||||
"psr/log": "^1|^2|^3"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "14.0.0",
|
||||
"fig/log-test": "^1",
|
||||
"jetbrains/phpstorm-stubs": "2023.2",
|
||||
"phpstan/phpstan": "2.1.30",
|
||||
"phpstan/phpstan-phpunit": "2.0.7",
|
||||
"phpstan/phpstan-strict-rules": "^2",
|
||||
"phpunit/phpunit": "11.5.56",
|
||||
"slevomat/coding-standard": "8.31.1",
|
||||
"squizlabs/php_codesniffer": "4.0.4",
|
||||
"symfony/cache": "^6.3.8|^7.0|^8.0",
|
||||
"symfony/console": "^5.4|^6.3|^7.0|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/console": "For helpful console commands such as SQL execution and import of files."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\DBAL\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Guilherme Blanco",
|
||||
"email": "guilhermeblanco@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Roman Borschel",
|
||||
"email": "roman@code-factory.org"
|
||||
},
|
||||
{
|
||||
"name": "Benjamin Eberlei",
|
||||
"email": "kontakt@beberlei.de"
|
||||
},
|
||||
{
|
||||
"name": "Jonathan Wage",
|
||||
"email": "jonwage@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
|
||||
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
|
||||
"keywords": [
|
||||
"abstraction",
|
||||
"database",
|
||||
"db2",
|
||||
"dbal",
|
||||
"mariadb",
|
||||
"mssql",
|
||||
"mysql",
|
||||
"oci8",
|
||||
"oracle",
|
||||
"pdo",
|
||||
"pgsql",
|
||||
"postgresql",
|
||||
"queryobject",
|
||||
"sasql",
|
||||
"sql",
|
||||
"sqlite",
|
||||
"sqlserver",
|
||||
"sqlsrv"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/dbal/issues",
|
||||
"source": "https://github.com/doctrine/dbal/tree/4.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.doctrine-project.org/sponsorship.html",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpdoctrine",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-09-24T16:11:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
"version": "1.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/deprecations.git",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpunit/phpunit": "<=7.5 || >=14"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^9 || ^12 || ^14",
|
||||
"phpstan/phpstan": "1.4.10 || 2.1.30",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
|
||||
"psr/log": "^1 || ^2 || ^3"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Deprecations\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
|
||||
"homepage": "https://www.doctrine-project.org/",
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/deprecations/issues",
|
||||
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
|
||||
},
|
||||
"time": "2026-02-07T07:09:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/inflector",
|
||||
"version": "2.1.0",
|
||||
|
|
@ -2053,6 +2358,61 @@
|
|||
},
|
||||
"time": "2026-09-02T08:00:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-aws-s3-v3",
|
||||
"version": "3.35.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
|
||||
"reference": "b03780cb97585ee7e48977f40ed599b33b751634"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/b03780cb97585ee7e48977f40ed599b33b751634",
|
||||
"reference": "b03780cb97585ee7e48977f40ed599b33b751634",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"aws/aws-sdk-php": "^3.371.5",
|
||||
"league/flysystem": "^3.10.0",
|
||||
"league/mime-type-detection": "^1.0.0",
|
||||
"php": "^8.0.2"
|
||||
},
|
||||
"conflict": {
|
||||
"guzzlehttp/guzzle": "<7.0",
|
||||
"guzzlehttp/ringphp": "<1.1.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\AwsS3V3\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frankdejonge.nl"
|
||||
}
|
||||
],
|
||||
"description": "AWS S3 filesystem adapter for Flysystem.",
|
||||
"keywords": [
|
||||
"Flysystem",
|
||||
"aws",
|
||||
"file",
|
||||
"files",
|
||||
"filesystem",
|
||||
"s3",
|
||||
"storage"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.35.3"
|
||||
},
|
||||
"time": "2026-08-08T16:19:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem-local",
|
||||
"version": "3.35.3",
|
||||
|
|
@ -2445,6 +2805,72 @@
|
|||
],
|
||||
"time": "2026-09-29T19:21:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mtdowling/jmespath.php",
|
||||
"version": "2.9.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jmespath/jmespath.php.git",
|
||||
"reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/2157c5e50e813ec6a96c1eed3be7f64a20fb32a8",
|
||||
"reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"symfony/polyfill-mbstring": "^1.17"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/xdebug-handler": "^3.0.3",
|
||||
"phpunit/phpunit": "^8.5.52"
|
||||
},
|
||||
"bin": [
|
||||
"bin/jp.php"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.9-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/JmesPath.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"JmesPath\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
},
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "Declaratively specify how to extract elements from a JSON document",
|
||||
"keywords": [
|
||||
"json",
|
||||
"jsonpath"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/jmespath/jmespath.php/issues",
|
||||
"source": "https://github.com/jmespath/jmespath.php/tree/2.9.2"
|
||||
},
|
||||
"time": "2026-07-06T18:56:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "3.14.2",
|
||||
|
|
@ -2927,6 +3353,55 @@
|
|||
],
|
||||
"time": "2026-08-24T00:54:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
"version": "3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/cache.git",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Cache\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for caching libraries",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"psr",
|
||||
"psr-6"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/cache/tree/3.0.0"
|
||||
},
|
||||
"time": "2021-02-03T23:26:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/clock",
|
||||
"version": "1.0.0",
|
||||
|
|
@ -4135,6 +4610,76 @@
|
|||
],
|
||||
"time": "2026-06-05T06:23:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.4.21",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/fd0e3462be6d32a0d130e8423e4940a7688162d8",
|
||||
"reference": "fd0e3462be6d32a0d130e8423e4940a7688162d8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/process": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Filesystem\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.4.21"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-10-06T20:05:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.4.20",
|
||||
|
|
@ -6767,54 +7312,6 @@
|
|||
],
|
||||
"time": "2024-05-06T16:37:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/deprecations",
|
||||
"version": "1.1.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/deprecations.git",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpunit/phpunit": "<=7.5 || >=14"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^9 || ^12 || ^14",
|
||||
"phpstan/phpstan": "1.4.10 || 2.1.30",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
|
||||
"psr/log": "^1 || ^2 || ^3"
|
||||
},
|
||||
"suggest": {
|
||||
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Deprecations\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
|
||||
"homepage": "https://www.doctrine-project.org/",
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/deprecations/issues",
|
||||
"source": "https://github.com/doctrine/deprecations/tree/1.1.6"
|
||||
},
|
||||
"time": "2026-02-07T07:09:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.24.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Dépôt de fichier pour les devoirs maison (bloc B, stockage S3, 2026-10-14) :
|
||||
* en plus du texte libre, pas à sa place — un rendu a au moins l'un des
|
||||
* deux. Le fichier n'est jamais sur le serveur, seulement son chemin sur
|
||||
* Object Storage (Hetzner, disque « s3 »).
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('rendus_devoirs', function (Blueprint $table) {
|
||||
$table->text('reponse')->nullable()->change();
|
||||
$table->string('fichier_chemin')->nullable()->after('reponse');
|
||||
$table->string('fichier_nom_original')->nullable()->after('fichier_chemin');
|
||||
$table->unsignedInteger('fichier_taille_octets')->nullable()->after('fichier_nom_original');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('rendus_devoirs', function (Blueprint $table) {
|
||||
$table->dropColumn(['fichier_chemin', 'fichier_nom_original', 'fichier_taille_octets']);
|
||||
$table->text('reponse')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -26,14 +26,26 @@
|
|||
<p class="mt-2 font-semibold text-[#5f6368]">Rendu le {{ $rendu->rendu_a->timezone(config('app.fuseau_utilisateurs'))->translatedFormat('d/m/Y H:i') }}. Un nouvel envoi remplace celui-ci{{ $devoir->echeanceDepassee() ? '' : ', jusqu\'à l\'échéance' }}.</p>
|
||||
@endif
|
||||
@error('reponse')<p class="mt-2 font-bold text-[#A63D2A]">{{ $message }}</p>@enderror
|
||||
@error('fichier')<p class="mt-2 font-bold text-[#A63D2A]">{{ $message }}</p>@enderror
|
||||
@if ($rendu && $rendu->aUnFichier())
|
||||
<p class="mt-2 font-semibold text-[#5f6368]">Fichier déjà envoyé : {{ $rendu->fichier_nom_original }}</p>
|
||||
@endif
|
||||
@if (! $devoir->echeanceDepassee())
|
||||
<form action="{{ route('devoirs.eleve.rendre', $devoir) }}" method="post" class="mt-4 grid gap-3">
|
||||
<form action="{{ route('devoirs.eleve.rendre', $devoir) }}" method="post" enctype="multipart/form-data" class="mt-4 grid gap-3">
|
||||
@csrf
|
||||
<textarea name="reponse" required rows="8" class="rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold">{{ old('reponse', $rendu->reponse ?? '') }}</textarea>
|
||||
<textarea name="reponse" rows="8" class="rounded-xl border border-[#c4c7c5] bg-white px-3 py-2 font-semibold" placeholder="Ta réponse (facultatif si tu joins un fichier)">{{ old('reponse', $rendu->reponse ?? '') }}</textarea>
|
||||
@if ($depotFichierDisponible)
|
||||
<label class="font-bold">
|
||||
Joindre un fichier (facultatif)
|
||||
<input type="file" name="fichier" class="mt-1 block font-semibold">
|
||||
</label>
|
||||
@endif
|
||||
<button type="submit" class="mt-2 inline-flex h-11 w-fit items-center rounded-full bg-black px-5 font-bold text-white">Envoyer</button>
|
||||
</form>
|
||||
@elseif ($rendu)
|
||||
<p class="mt-4 whitespace-pre-wrap font-semibold">{{ $rendu->reponse }}</p>
|
||||
@if ($rendu->reponse)
|
||||
<p class="mt-4 whitespace-pre-wrap font-semibold">{{ $rendu->reponse }}</p>
|
||||
@endif
|
||||
@endif
|
||||
</section>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -32,7 +32,14 @@
|
|||
@endif
|
||||
</div>
|
||||
@if ($rendu)
|
||||
<p class="mt-3 whitespace-pre-wrap rounded-xl border border-[#e3e5e8] bg-[#f8f9fa] p-4 font-semibold">{{ $rendu->reponse }}</p>
|
||||
@if ($rendu->reponse)
|
||||
<p class="mt-3 whitespace-pre-wrap rounded-xl border border-[#e3e5e8] bg-[#f8f9fa] p-4 font-semibold">{{ $rendu->reponse }}</p>
|
||||
@endif
|
||||
@if ($rendu->aUnFichier())
|
||||
<a href="{{ route('mes-classes.devoirs.telecharger', [$classe, $devoir, $rendu]) }}" class="mt-3 inline-flex items-center gap-2 rounded-xl border border-[#e3e5e8] bg-[#f8f9fa] px-4 py-2 font-bold underline underline-offset-4">
|
||||
Fichier : {{ $rendu->fichier_nom_original }} ({{ number_format($rendu->fichier_taille_octets / 1024, 0) }} Ko)
|
||||
</a>
|
||||
@endif
|
||||
<form action="{{ route('mes-classes.devoirs.noter', [$classe, $devoir, $rendu]) }}" method="post" class="mt-3 flex items-center gap-2">
|
||||
@csrf
|
||||
<label class="font-bold">Note <input type="number" name="note" min="0" max="20" value="{{ $rendu->note }}" class="w-20 rounded-xl border border-[#c4c7c5] bg-white px-2 py-1 font-semibold"> / 20</label>
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ Route::middleware(['auth:web', 'session.unique:web'])->group(function () {
|
|||
Route::get('/mes-classes/{classe}/devoirs/{devoir}', [DevoirController::class, 'show'])->name('mes-classes.devoirs.show');
|
||||
Route::post('/mes-classes/{classe}/devoirs/{devoir}/supprimer', [DevoirController::class, 'destroy'])->name('mes-classes.devoirs.destroy');
|
||||
Route::post('/mes-classes/{classe}/devoirs/{devoir}/rendus/{rendu}/noter', [DevoirController::class, 'noter'])->name('mes-classes.devoirs.noter');
|
||||
Route::get('/mes-classes/{classe}/devoirs/{devoir}/rendus/{rendu}/telecharger', [DevoirController::class, 'telechargerRendu'])->name('mes-classes.devoirs.telecharger');
|
||||
|
||||
// Séparation vitrine/application (2026-10-13) : Ma classe (annonces),
|
||||
// Résultats (observations), Séances en direct, Bibliothèque, chat.
|
||||
|
|
|
|||
147
portail/tests/Feature/StockageDevoirsTest.php
Normal file
147
portail/tests/Feature/StockageDevoirsTest.php
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\Devoir;
|
||||
use App\Models\Eleve;
|
||||
use App\Models\RenduDevoir;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/** Active le disque s3 factice et le fait passer pour configuré. */
|
||||
function avecDisqueS3Factice(): void
|
||||
{
|
||||
config(['filesystems.disks.s3.bucket' => 'bucket-de-test']);
|
||||
Storage::fake('s3');
|
||||
}
|
||||
|
||||
test('tant que le bucket s3 n’est pas configuré, le dépôt de fichier est masqué et refusé', function () {
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
$devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]);
|
||||
|
||||
eleve($this, $eleve)->get(route('devoirs.eleve.show', $devoir))->assertOk()->assertDontSee('Joindre un fichier');
|
||||
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('devoir.pdf', 100, 'application/pdf'),
|
||||
])->assertStatus(503);
|
||||
});
|
||||
|
||||
test('un élève peut rendre un devoir avec un fichier seul, sans texte', function () {
|
||||
avecDisqueS3Factice();
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
$devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]);
|
||||
|
||||
eleve($this, $eleve)->get(route('devoirs.eleve.show', $devoir))->assertOk()->assertSee('Joindre un fichier');
|
||||
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('devoir.pdf', 100, 'application/pdf'),
|
||||
])->assertRedirect();
|
||||
|
||||
$rendu = RenduDevoir::where('devoir_id', $devoir->id)->where('eleve_id', $eleve->id)->first();
|
||||
expect($rendu)->not->toBeNull();
|
||||
expect($rendu->reponse)->toBeNull();
|
||||
expect($rendu->aUnFichier())->toBeTrue();
|
||||
expect($rendu->fichier_nom_original)->toBe('devoir.pdf');
|
||||
Storage::disk('s3')->assertExists($rendu->fichier_chemin);
|
||||
});
|
||||
|
||||
test('un rendu sans réponse ni fichier est refusé', function () {
|
||||
avecDisqueS3Factice();
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
$devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]);
|
||||
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [])->assertSessionHasErrors(['reponse', 'fichier']);
|
||||
expect(RenduDevoir::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('un fichier trop lourd ou d’une extension refusée est rejeté', function () {
|
||||
avecDisqueS3Factice();
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
$devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]);
|
||||
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('trop-gros.pdf', 11 * 1024, 'application/pdf'),
|
||||
])->assertSessionHasErrors('fichier');
|
||||
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('script.exe', 10, 'application/x-msdownload'),
|
||||
])->assertSessionHasErrors('fichier');
|
||||
|
||||
expect(RenduDevoir::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('remplacer son fichier supprime l’ancien de l’object storage, un nouvel envoi en texte seul le conserve', function () {
|
||||
avecDisqueS3Factice();
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
$devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]);
|
||||
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('premier.pdf', 100, 'application/pdf'),
|
||||
])->assertRedirect();
|
||||
$cheminPremier = RenduDevoir::first()->fichier_chemin;
|
||||
Storage::disk('s3')->assertExists($cheminPremier);
|
||||
|
||||
// Un nouvel envoi en texte seul (pas de nouveau fichier) garde l'ancien fichier.
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), ['reponse' => 'Je précise ma réponse'])->assertRedirect();
|
||||
$rendu = RenduDevoir::first();
|
||||
expect($rendu->fichier_chemin)->toBe($cheminPremier)->and($rendu->reponse)->toBe('Je précise ma réponse');
|
||||
Storage::disk('s3')->assertExists($cheminPremier);
|
||||
|
||||
// Un nouveau fichier remplace l'ancien, qui est supprimé de l'object storage.
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('second.pdf', 100, 'application/pdf'),
|
||||
])->assertRedirect();
|
||||
Storage::disk('s3')->assertMissing($cheminPremier);
|
||||
expect(RenduDevoir::count())->toBe(1);
|
||||
});
|
||||
|
||||
test('seul le prof propriétaire peut télécharger le fichier rendu par un élève', function () {
|
||||
avecDisqueS3Factice();
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
$devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]);
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('devoir.pdf', 100, 'application/pdf'),
|
||||
])->assertRedirect();
|
||||
$rendu = RenduDevoir::first();
|
||||
|
||||
commeProf($this, $classe->prof)->get(route('mes-classes.devoirs.telecharger', [$classe, $devoir, $rendu]))->assertRedirect();
|
||||
|
||||
$autreClasse = Classe::factory()->create();
|
||||
commeProf($this, $autreClasse->prof)->get(route('mes-classes.devoirs.telecharger', [$classe, $devoir, $rendu]))->assertForbidden();
|
||||
});
|
||||
|
||||
test('le prof voit le fichier déposé par un élève dans la page du devoir', function () {
|
||||
avecDisqueS3Factice();
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
$devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]);
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('rapport-reseau.pdf', 100, 'application/pdf'),
|
||||
])->assertRedirect();
|
||||
|
||||
commeProf($this, $classe->prof)->get(route('mes-classes.devoirs.show', [$classe, $devoir]))
|
||||
->assertSee('rapport-reseau.pdf');
|
||||
});
|
||||
|
||||
test('purger une classe archivée supprime aussi les fichiers de ses rendus sur l’object storage', function () {
|
||||
avecDisqueS3Factice();
|
||||
$classe = Classe::factory()->create();
|
||||
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
|
||||
$devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]);
|
||||
eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), [
|
||||
'fichier' => UploadedFile::fake()->create('devoir.pdf', 100, 'application/pdf'),
|
||||
])->assertRedirect();
|
||||
$chemin = RenduDevoir::first()->fichier_chemin;
|
||||
Storage::disk('s3')->assertExists($chemin);
|
||||
|
||||
$classe->forceFill(['archivee_a' => now()->subDays(91)])->save();
|
||||
$this->artisan('classes:purger-archivees');
|
||||
|
||||
Storage::disk('s3')->assertMissing($chemin);
|
||||
expect(RenduDevoir::count())->toBe(0);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue