Une séance peut être lancée en mode examen avec une durée dédiée. Réutilise le mécanisme d'expiration déjà en place (labs:expirer-sessions, expire_a) plutôt que d'en construire un nouveau. Le navigateur de l'élève signale les changements d'onglet et les sorties de plein écran (evenements_examen : un type et un horodatage, rien d'autre - pas de capture d'écran ni de webcam) ; le prof voit le compte par élève et le temps restant dans le cockpit de sa classe et la salle d'attente. Choix d'Adam après discussion des options (suivi chronométré simple / + détection d'onglet / + tentative de plein écran) : la troisième, honnêtement présentée - un navigateur ne permet pas de vraiment empêcher de sortir, seulement de le signaler. Avec cette étape, les quatre fonctions fictives du tableau de bord démo (quiz, devoirs, observations, examen) sont maintenant réelles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
110 lines
3.2 KiB
PHP
110 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class LabSession extends Model
|
|
{
|
|
public const EN_ATTENTE = 'en_attente';
|
|
|
|
// Admis par le prof : CreerStackLab est dispatché, la stack Heat se construit.
|
|
public const ADMIS = 'admis';
|
|
|
|
// Stack CREATE_COMPLETE : les VM existent, les terminaux peuvent s'ouvrir.
|
|
public const ACTIF = 'actif';
|
|
|
|
public const TERMINE = 'termine';
|
|
|
|
public const EXPIRE = 'expire';
|
|
|
|
// Trois créations de stack ratées de suite (voir CreerStackLab) : le prof
|
|
// le voit dans la salle d'attente et peut relancer.
|
|
public const ECHEC = 'echec';
|
|
|
|
protected $fillable = ['seance_id', 'eleve_id', 'lab_id', 'heat_stack_id', 'heat_stack_name', 'etat', 'raison_echec', 'expire_a'];
|
|
|
|
protected $casts = [
|
|
'expire_a' => 'datetime',
|
|
];
|
|
|
|
/** @return BelongsTo<Seance, $this> */
|
|
public function seance(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Seance::class);
|
|
}
|
|
|
|
/** @return BelongsTo<Eleve, $this> */
|
|
public function eleve(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Eleve::class);
|
|
}
|
|
|
|
/** @return BelongsTo<Lab, $this> */
|
|
public function lab(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Lab::class);
|
|
}
|
|
|
|
/** @return HasMany<LabSessionMachine, $this> */
|
|
public function machines(): HasMany
|
|
{
|
|
return $this->hasMany(LabSessionMachine::class);
|
|
}
|
|
|
|
/**
|
|
* Relie une stack prête à cette session : venue du stock chaud
|
|
* (SeanceController::admettre) ou construite à la demande (CreerStackLab).
|
|
*
|
|
* @param array{nom: string, id: string, instances: array<string, string>} $stack
|
|
*/
|
|
public function attribuerStack(array $stack): void
|
|
{
|
|
DB::transaction(function () use ($stack) {
|
|
$this->machines()->delete();
|
|
|
|
foreach ($stack['instances'] as $nom => $instanceId) {
|
|
$this->machines()->create(['nom' => $nom, 'instance_id' => $instanceId]);
|
|
}
|
|
|
|
$this->forceFill([
|
|
'heat_stack_name' => $stack['nom'],
|
|
'heat_stack_id' => $stack['id'],
|
|
'etat' => self::ACTIF,
|
|
'raison_echec' => null,
|
|
])->save();
|
|
});
|
|
}
|
|
|
|
/** @return HasMany<Correction, $this> */
|
|
public function corrections(): HasMany
|
|
{
|
|
return $this->hasMany(Correction::class);
|
|
}
|
|
|
|
/** @return HasOne<Correction, $this> */
|
|
public function derniereCorrection(): HasOne
|
|
{
|
|
return $this->hasOne(Correction::class)->latestOfMany();
|
|
}
|
|
|
|
/** @return HasMany<EvenementExamen, $this> */
|
|
public function evenementsExamen(): HasMany
|
|
{
|
|
return $this->hasMany(EvenementExamen::class)->latest();
|
|
}
|
|
|
|
/** Nul si la session n'expire jamais ou si l'expiration est déjà passée. */
|
|
public function minutesRestantes(): ?int
|
|
{
|
|
if ($this->expire_a === null || $this->expire_a->isPast()) {
|
|
return null;
|
|
}
|
|
|
|
return (int) ceil(now()->diffInSeconds($this->expire_a) / 60);
|
|
}
|
|
}
|