<?php
namespace App\Entity;
use App\Entity\Traits\Timestampable;
use App\Repository\StudentAnswerRepository;
use Doctrine\ORM\Mapping as ORM;
// Réponse d'un élève à une Question, dans le cadre d'une StudentActivityAttempt.
// `rawAnswer` porte soit du texte libre (short_answer), soit un JSON encodé
// (tableau d'ids QuestionOption sélectionnés pour qcm/true_false/matching, ou
// tableau d'ids dans l'ordre proposé par l'élève pour ordering).
#[ORM\HasLifecycleCallbacks]
#[ORM\Entity(repositoryClass: StudentAnswerRepository::class)]
class StudentAnswer
{
use Timestampable;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'answers')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?StudentActivityAttempt $attempt = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?Question $question = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $rawAnswer = null;
#[ORM\Column(nullable: true)]
private ?bool $isCorrect = null;
#[ORM\Column(nullable: true)]
private ?float $pointsEarned = null;
// Surclassement manuel par un enseignant, pour les réponses ouvertes
// (short_answer) que la correction automatique (exact/ci/contains) juge
// insatisfaisant. Quand renseigné, prime sur pointsEarned/isCorrect pour
// le calcul du score de la tentative (voir CapsuleGradingService).
#[ORM\Column(nullable: true)]
private ?float $teacherPointsEarned = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $teacherFeedback = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?User $gradedBy = null;
#[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $gradedAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getAttempt(): ?StudentActivityAttempt
{
return $this->attempt;
}
public function setAttempt(?StudentActivityAttempt $attempt): static
{
$this->attempt = $attempt;
return $this;
}
public function getQuestion(): ?Question
{
return $this->question;
}
public function setQuestion(?Question $question): static
{
$this->question = $question;
return $this;
}
public function getRawAnswer(): ?string
{
return $this->rawAnswer;
}
public function setRawAnswer(?string $rawAnswer): static
{
$this->rawAnswer = $rawAnswer;
return $this;
}
public function isCorrect(): ?bool
{
return $this->isCorrect;
}
public function setIsCorrect(?bool $isCorrect): static
{
$this->isCorrect = $isCorrect;
return $this;
}
public function getPointsEarned(): ?float
{
return $this->pointsEarned;
}
public function setPointsEarned(?float $pointsEarned): static
{
$this->pointsEarned = $pointsEarned;
return $this;
}
public function getTeacherPointsEarned(): ?float
{
return $this->teacherPointsEarned;
}
public function setTeacherPointsEarned(?float $teacherPointsEarned): static
{
$this->teacherPointsEarned = $teacherPointsEarned;
return $this;
}
public function getTeacherFeedback(): ?string
{
return $this->teacherFeedback;
}
public function setTeacherFeedback(?string $teacherFeedback): static
{
$this->teacherFeedback = $teacherFeedback;
return $this;
}
public function getGradedBy(): ?User
{
return $this->gradedBy;
}
public function setGradedBy(?User $gradedBy): static
{
$this->gradedBy = $gradedBy;
return $this;
}
public function getGradedAt(): ?\DateTimeInterface
{
return $this->gradedAt;
}
public function setGradedAt(?\DateTimeInterface $gradedAt): static
{
$this->gradedAt = $gradedAt;
return $this;
}
/**
* Score effectif de cette réponse : la correction manuelle prime sur le
* calcul automatique quand elle a été saisie.
*/
public function getEffectivePointsEarned(): ?float
{
return $this->teacherPointsEarned ?? $this->pointsEarned;
}
/**
* true seulement pour les réponses ouvertes (short_answer) — les seules
* pouvant faire l'objet d'une correction manuelle (QCM/vrai-faux/
* appariement/ordonnancement restent toujours entièrement auto-corrigés).
*/
public function isManuallyGradable(): bool
{
return $this->question?->getType() === Activity::TYPE_SHORT_ANSWER;
}
}