<?php
declare(strict_types=1);
namespace CoreBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
use StudentBundle\Service\Transform;
/**
* @ORM\Table("mathproblem")
* @ORM\Entity(repositoryClass="MathproblemRepository")
*/
class Mathproblem implements JsonSerializable, HasMediaInterface
{
use HasMediaTraits;
const MATH_PROBLEM_QUIZ = 1;
const MATH_PROBLEM_WORKSHEET = 2;
const MATH_PROBLEM_QUIZ_AND_WORKSHEET = 3;
const STATUS_PUBLISHED = 0;
const STATUS_UNPUBLISHED = 1;
const STATUS_DELETED = 2;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var bool
* @ORM\Column(name="validated", type="boolean", nullable=true)
*/
private $validated;
/**
* @var string
*
* @ORM\Column(name="name", type="string", nullable=false)
*/
private $name;
/**
* @var string
*
* @ORM\Column(name="description", type="string", length=8000, nullable=true)
*/
private $description;
/**
* The possible choices for this Mathproblem, will only be populated when type is 'multi'.
*
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="Answer", mappedBy="mathproblem", cascade={"persist", "remove"})
*/
private $answers;
/**
* @var ArrayCollection
*
* @ORM\OneToMany(targetEntity="BugReport", mappedBy="problem", cascade={"persist", "remove"})
*/
private $bugReports;
/**
* @var string
*
* @ORM\Column(name="mp_image", type="string", nullable=true)
*/
private $mpImage;
// @TODO delete
/**
* @ORM\ManyToOne(targetEntity="Video")
* @ORM\JoinColumn(nullable=true)
*/
private ?Video $video;
/**
* @var string
* @ORM\Column(name="hint", type="string", length=2000, nullable=true)
*/
private $hint;
/**
* @var string
* @ORM\Column(name="video_hint", type="string", length=300, nullable=true)
*/
private $videoHint;
/**
* @var string
* @ORM\Column(name="solution", type="string", length=3000, nullable=true)
*/
private $solution;
/**
* @var string
*
* @ORM\Column(name="free_text", type="text", nullable=true)
*/
private $freeText;
/**
* @var string
*
* @ORM\Column(name="intro_text", type="text", nullable=true)
*/
private $introText;
/**
* Mathproblem template
*
* @ORM\ManyToOne(targetEntity="TemplateMathproblem", inversedBy="mathproblems")
* @ORM\JoinColumn(name="template_id", referencedColumnName="id")
*/
private $template;
/**
* Relative path to the GeoGebra problem script file.
*
* @var string
*
* @ORM\Column(type="string", nullable=true)
*/
private $geogebra;
/**
* 0 - available/published
* 1 - unpublished
* 2 - deleted
*
* @var ?int
* @ORM\Column(name="status", type="integer", nullable=true)
*/
private $status;
/**
* @var int
* @ORM\Column(name="subquestion_nr", type="smallint", nullable=true)
*/
private $subquestionNr;
/**
* Flag used to check if there's any next or previous problem
* @var bool
*/
private $hasNext;
/**
* Flag used to control if the problem should be searchable in a quiz
* @var bool
* @ORM\Column(name="allow_in_quiz_or_worksheet", type="boolean", nullable=true)
*/
private $allowInQuizOrWorksheet;
/**
* @ORM\OneToMany(targetEntity="RelationQuizMathproblem", mappedBy="mathproblem")
*/
private Collection $quizzes;
/**
* @var ArrayCollection
*
* @ORM\ManyToMany(targetEntity="Appendix", inversedBy="problems", cascade={"persist", "remove"})
* @ORM\JoinTable(name="problems_appendices",
* joinColumns={@ORM\JoinColumn(name="problem_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="appendix_id", referencedColumnName="id")})
*/
private $appendices;
/**
* @ORM\OneToOne(targetEntity="VideoReference", inversedBy="mathproblem")
* @ORM\JoinColumn(name="video_reference", referencedColumnName="id", nullable=true)
*/
private ?VideoReference $videoReference;
/**
* @var \DateTime
*
* @ORM\Column(name="createdAt", type="datetime", nullable=false)
*/
private \DateTime $createdAt;
/**
* 0 = default view
* 1,2,3,... = alternate views, that can vary by problem type
*
* @var int
* @ORM\Column(name="view_mode", type="smallint", options={"default" : 0})
*/
private int $viewMode = 0;
public array $excludeKeys = [];
protected bool $useTransform = false;
protected Transform $transform;
/**
* @ORM\OneToOne(targetEntity="ProblemDnd", mappedBy="problem")
*/
private $problemDnd;
static function setTransform(Transform $transform){
self::$transform = $transform;
}
public function __construct()
{
$this->answers = new ArrayCollection();
$this->appendices = new ArrayCollection();
$this->quizzes = new ArrayCollection();
$this->imageReference = null;
$this->videoReference = null;
$this->audioReference = null;
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
public function setName(string $name)
{
$this->name = $name;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description ?? '';
}
public function setDescription(string $description)
{
$this->description = $description;
}
public function getAnswers(): Collection
{
return $this->answers;
}
public function getHint(): ?string
{
return $this->hint;
}
public function setHint(?string $hint)
{
$this->hint = $hint;
}
/**
* @return string|null
*/
public function getVideoHint(): ?string
{
return $this->videoHint;
}
/**
* @param string|null $videoHint
*/
public function setVideoHint(?string $videoHint): void
{
$this->videoHint = $videoHint;
}
public function getSolution(): ?string
{
return $this->solution;
}
public function setSolution(?string $solution)
{
$this->solution = $solution;
}
/**
* @return TemplateMathproblem
*/
public function getTemplate()
{
return $this->template;
}
public function setTemplate(TemplateMathproblem $template)
{
$this->template = $template;
}
/**
* @return string
*/
public function getMpImage()
{
// @TODO delete
return 'Obsolete, use getImagePath() instead';
}
/**
* @return ?int
*/
public function getStatus()
{
return $this->status;
}
/**
* @param ?int $status
*/
public function setStatus($status)
{
$this->status = $status;
}
/**
* Status helper function
*/
public function isPublished(): bool
{
// Treating null as published since status is nullable, and it would have been treated like this before, though it should not happen.
return $this->status === self::STATUS_PUBLISHED || $this->status === null;
}
/**
* Status helper function
*/
public function setPublished(): void
{
$this->status = self::STATUS_PUBLISHED;
}
/**
* Status helper function
*/
public function isUnpublished(): bool
{
return $this->status === self::STATUS_UNPUBLISHED;
}
/**
* Status helper function
*/
public function setUnpublished(): void
{
$this->status = self::STATUS_UNPUBLISHED;
}
/**
* Status helper function
*/
public function isDeleted(): bool
{
return $this->status === self::STATUS_DELETED;
}
/**
* Status helper function
*/
public function setDeleted(): void
{
$this->status = self::STATUS_DELETED;
}
/**
* @return int
*/
public function getSubquestionNr()
{
return $this->subquestionNr;
}
/**
* @param int $subquestionNr
*
* Specify if the math problem is part of a bigger question with several subquestions
*
*/
public function setSubquestionNr($subquestionNr)
{
$this->subquestionNr = $subquestionNr;
}
public function getGeogebra(): ?string
{
return $this->geogebra;
}
/**
* @return string|null
*/
public function getFreeText(): ?string
{
return $this->freeText;
}
/**
* @param string|null $freeText
*/
public function setFreeText(?string $freeText): void
{
$this->freeText = $freeText;
}
/**
* @return string|null
*/
public function getIntroText(): ?string
{
return $this->introText;
}
/**
* @param string|null $introText
*/
public function setIntroText(?string $introText): void
{
$this->introText = $introText;
}
public function getCorrectAnswer(): ?Answer
{
$correctAnswer = null;
foreach ($this->answers as $answer) {
if ($answer->getIsCorrect()) {
$correctAnswer = $answer;
break;
}
}
return $correctAnswer;
}
public function getWrongAnswer(): ?Answer
{
$wrongAnswer = null;
foreach ($this->answers as $answer) {
if (!$answer->getIsCorrect()) {
$wrongAnswer = $answer;
break;
}
}
return $wrongAnswer;
}
/**
* @return array|null
*/
public function getCorrectAnswers(): ?array
{
// TODO - Jon's part, try to optimize for reusing
$answerString = $this->getCorrectAnswer()->getAnswerText();
$type = $this->getTemplate()->getType();
if ($type === TemplateMathproblem::TYPE_DROPDOWN) {
// Get correct answers dynamically
$stringArray = explode("#", $answerString);
$correctAnswers = [];
$index = 0;
foreach ($stringArray as $str) {
// if string contains '|', it contains dropdown options
if (!(strpos($str, '|') === false)) {
$dropdownChoices = explode("|", $str);
$correctAnswers[$index] = $dropdownChoices[0];
foreach ($dropdownChoices as $choice){
if(preg_match("#^\*#", $choice) || preg_match("#\*$#", $choice)){
$choice = preg_replace("#^\*#", "", $choice);
$choice = preg_replace("#\*$#", "", $choice);
$correctAnswers[$index] = $choice;
}
}
$index++;
}
}
return $correctAnswers;
} elseif ($type === TemplateMathproblem::TYPE_WORD_OPTIONS) {
$stringArray = explode("#", $answerString);
$correctAnswers = [];
for ($i = 1; $i < count($stringArray); $i += 2) {
$str = $stringArray[$i];
if (in_array($str, ['<br>', '<br/>'])) {
continue;
}
if (strpos($str, '|') === false) {
$correctAnswers[] = 'default';
} else {
$correctAnswers[] = explode('|', $str)[1];
}
}
return $correctAnswers;
} else {
// TODO: perhaps make this possible for other template types too.
return null;
}
}
/**
* @return bool
*/
public function getHasNext(){
return $this->hasNext;
}
public function setHasNext($hasNext){
$this->hasNext = $hasNext;
}
public function getAppendices(): Collection
{
return $this->appendices;
}
public function setAppendices(ArrayCollection $appendices): void
{
$this->appendices = $appendices;
}
public function getVideo(): ?Video
{
return $this->video;
}
function jsonSerialize(): mixed
{
// Initialize the $video property if it's not already initialized.
$this->video ??= null;
$data = [
"id" => $this->getId(),
"name" => $this->getName(),
"description" => $this->getDescription(),
"introText" => $this->getIntroText(),
"freeText" => $this->getFreeText(),
"hint" => $this->getHint(),
"correctAnswer" => $this->getCorrectAnswer(),
"template" => $this->getTemplate(),
"status" => $this->getStatus(),
"subquestionNr" => $this->getSubquestionNr(),
"georgeBra" => $this->getGeogebra(),
"solution" => $this->getSolution(),
"image" => $this->getImagePath(),
"appendices" => $this->getAppendices()->toArray(),
"hasNext" => $this->getHasNext(),
"video" => $this->getVideo(),
"wrongAnswer" => $this->getWrongAnswer(),
"topicDisplayName" => $this->getTemplate()->getTopic()->getDisplayName(),
"options" => $this->getCorrectAnswer() && $this->getCorrectAnswer()->getAnswerText() ? explode("#", $this->getCorrectAnswer()->getAnswerText()) : [],
"viewMode" => $this->getViewMode(),
];
for ($i=0, $count = count($this->excludeKeys); $i < $count; $i++){
if(isset( $data[ $this->excludeKeys[$i] ] )){
unset( $data[ $this->excludeKeys[$i] ] );
}
}
$answers = $this->getAnswers()->toArray();
if($this->getTemplate()->getShouldShuffle()){
shuffle($answers);
}
$data['answers'] = $answers;
return $data;
}
/**
* @param array $keys
* @return $this
*/
function exclude(array $keys): self{
$this->excludeKeys = $keys;
return $this;
}
/**
* @param bool $useTransform
* @return $this
*/
function setUseTransform(bool $useTransform = true) : self{
$this->useTransform = $useTransform;
return $this;
}
public function getAllowInQuizOrWorksheet(): ?bool
{
return $this->allowInQuizOrWorksheet;
}
public function setAllowInQuizOrWorksheet(?bool $allowInQuizOrWorksheet): void
{
$this->allowInQuizOrWorksheet = $allowInQuizOrWorksheet;
}
public function getViewMode(): int
{
return $this->viewMode;
}
public function getDifficulty() {
return $this->template->getDifficulty();
}
public function getCorrectAnswerText() {
return $this->getCorrectAnswer()?->getAnswerText();
}
/*
* Used for EasyAdmin problem form
*
* */
public function getTemplateId() {
return $this->getTemplate()->getId();
}
public function getType() {
return $this?->getTemplate()?->getType();
}
public function setType(int $type) {
$this->getTemplate()->setType($type);
}
public function getQuizzes(): Collection
{
$quizzes = new ArrayCollection();
foreach ($this->quizzes as $quiz) {
$quizzes->add($quiz->getQuiz());
}
return $quizzes;
}
public function getQuizId(): ?int {
$quizzes = $this->getQuizzes();
return $quizzes[0]?->getId();
}
public function getPublicationName(): string {
$quizzes = $this->getQuizzes();
return $quizzes[0]?->getPublication()?->getName();
}
public function isValidated(): bool
{
return $this->validated ?? false;
}
public function setValidated(bool $validated): void
{
$this->validated = $validated;
}
public function getProblemText(): string {
return '';
}
public function setProblemText(string $text) {
}
public function __toString(): string
{
return $this->name;
}
public function setViewMode(int $viewMode): void
{
$this->viewMode = $viewMode;
}
// generate a snippet of text containing the relevant information for the given problem
public function getSnippet(): string
{
$output = '';
if ($this->introText) {
$output .= '<br><b>Intro text:</b> ' . strip_tags($this->introText);
}
if ($this->description) {
$output .= '<br><b>Problem text:</b> ' . strip_tags($this->description);
}
if ($this->getType() === TemplateMathproblem::TYPE_DRAG_AND_DROP) {
$containers = $this->getProblemDnd()->getBins();
$containerList = $containers->map(function($container) {
$tmp = '';
// the correct answer might be text, audio and or image.
if ($container->getTxt())
{
$tmp .= $container->getTxt();
}
$tmp .= $container->displayMediaIfPresent();
return $tmp;
});
$output .= '<br><b>Containers:</b> ' . implode('|', $containerList->toArray() );
}
if ($this->getType() === TemplateMathproblem::TYPE_DRAG_AND_DROP || $this->getType() === TemplateMathproblem::TYPE_RADIO_BUTTON) {
// show all answers for dnd problems and radio button
$answers = $this->getAnswers();
$answerList = $answers->map(function($answer) {
$tmp = '';
// the correct answer might be text, audio and or image.
if ($answer->getAnswerText())
{
$tmp .= $answer->getAnswerText();
}
$tmp .= $answer->displayMediaIfPresent();
return $tmp;
});
$output .= '<br><b>Answers:</b> ' . implode('|', $answerList->toArray() );
}
else {
$output .= $this->getCorrectAnswer()?->getAnswerText() ?? '';
}
return preg_replace('/^<br>/', '', $output);
}
/**
* @return ?ProblemDnd
*/
public function getProblemDnd()
{
return $this->problemDnd;
}
public function getVideoReference(): ?VideoReference
{
return $this->videoReference;
}
public function setVideoReference(?VideoReference $videoReference): void
{
$this->videoReference = $videoReference;
}
public function isValidatedAndHasMediaFiles(): bool
{
if (!$this->isValidated()) {
return false;
}
// detect if problem has media files
if ($this->mediaCouplingExistButWithoutFile(ImageReference::class)) {
return false;
}
if ($this->mediaCouplingExistButWithoutFile(AudioReference::class)) {
return false;
}
// detect if answers have media files
foreach ($this->getAnswers() as $answer) {
if ($answer->mediaCouplingExistButWithoutFile(ImageReference::class)) {
return false;
}
if ($answer->mediaCouplingExistButWithoutFile(AudioReference::class)) {
return false;
}
}
// detect if containers have media files if dnd
if ($this->getType() === 4) {
$bins = $this->getProblemDnd()->getBins();
foreach ($bins as $bin) {
if ($bin->mediaCouplingExistButWithoutFile(ImageReference::class)) {
return false;
}
if ($bin->mediaCouplingExistButWithoutFile(AudioReference::class)) {
return false;
}
}
}
return $this->isValidated();
}
public function getActiveBugReports(): array
{
$bugReports = $this->bugReports;
$activeBugReports = [];
foreach ($bugReports as $bugReport) {
if ($bugReport->getDateResolved() === null) {
$activeBugReports[] = $bugReport->getId();
}
}
return $activeBugReports;
}
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
public function setCreatedAt(\DateTime $createdAt): void
{
$this->createdAt = $createdAt;
}
public function getTopic(): ?string
{
return $this->getTemplate()?->getTopic()?->getName();
}
public function setTopic(Topic $topic)
{
$this->getTemplate()?->setTopic($topic);
}
public function getCategory(): ?string
{
return $this->getTemplate()?->getTopic()?->getCategory()?->getName();
}
public function getImage(){
return $this->getImagePath();
}
public function getCmsImagePath(): string
{
if (!$this->imageReference) {
return 'Mangler';
}
if (!$this->imageReference->getFilePath()) {
return 'Mangler fil';
}
return $this->getImagePath();
}
public function getCmsAudioPath(): string
{
if (!$this->audioReference) {
return 'Mangler';
}
if (!$this->audioReference->getFilePath()) {
return 'Mangler fil';
}
return $this->getAudioPath();
}
}