src/Entity/Core/Mathproblem.php line 16

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Entity\Core;
  4. use App\Entity\Core\Topic\Topic;
  5. use Doctrine\Common\Collections\ArrayCollection;
  6. use Doctrine\Common\Collections\Collection;
  7. use Doctrine\ORM\Mapping as ORM;
  8. use JsonSerializable;
  9. use App\Services\Student\Transform;
  10. #[ORM\Table('mathproblem')]
  11. #[ORM\Entity(repositoryClass: MathproblemRepository::class)]
  12. class Mathproblem implements JsonSerializable, HasMediaInterface
  13. {
  14. use HasMediaTraits;
  15. const MATH_PROBLEM_QUIZ = 1;
  16. const MATH_PROBLEM_WORKSHEET = 2;
  17. const MATH_PROBLEM_QUIZ_AND_WORKSHEET = 3;
  18. const STATUS_PUBLISHED = 0;
  19. const STATUS_UNPUBLISHED = 1;
  20. const STATUS_DELETED = 2;
  21. /**
  22. * @var int
  23. */
  24. #[ORM\Column(name: 'id', type: 'integer')]
  25. #[ORM\Id]
  26. #[ORM\GeneratedValue(strategy: 'AUTO')]
  27. private $id;
  28. /**
  29. * @var bool
  30. */
  31. #[ORM\Column(name: 'validated', type: 'boolean', nullable: true)]
  32. private $validated;
  33. /**
  34. * @var string
  35. */
  36. #[ORM\Column(name: 'name', type: 'string', nullable: false)]
  37. private $name;
  38. /**
  39. * @var string
  40. */
  41. #[ORM\Column(name: 'description', type: 'string', length: 8000, nullable: true)]
  42. private $description;
  43. /**
  44. * The possible choices for this Mathproblem, will only be populated when type is 'multi'.
  45. *
  46. * @var ArrayCollection
  47. */
  48. #[ORM\OneToMany(targetEntity: \App\Entity\Core\Answer::class, mappedBy: 'mathproblem', cascade: ['persist', 'remove'])]
  49. private $answers;
  50. /**
  51. * @var ArrayCollection
  52. */
  53. #[ORM\OneToMany(targetEntity: \App\Entity\Core\BugReport::class, mappedBy: 'problem', cascade: ['persist', 'remove'])]
  54. private $bugReports;
  55. #[ORM\JoinColumn(nullable: true)]
  56. #[ORM\ManyToOne(targetEntity: \App\Entity\Core\Video::class)]
  57. private ?Video $video;
  58. /**
  59. * @var string
  60. */
  61. #[ORM\Column(name: 'hint', type: 'string', length: 2000, nullable: true)]
  62. private $hint;
  63. /**
  64. * @var string
  65. */
  66. #[ORM\Column(name: 'video_hint', type: 'string', length: 300, nullable: true)]
  67. private $videoHint;
  68. /**
  69. * @var string
  70. */
  71. #[ORM\Column(name: 'solution', type: 'string', length: 3000, nullable: true)]
  72. private $solution;
  73. /**
  74. * @var string
  75. */
  76. #[ORM\Column(name: 'free_text', type: 'text', nullable: true)]
  77. private $freeText;
  78. /**
  79. * @var string
  80. */
  81. #[ORM\Column(name: 'intro_text', type: 'text', nullable: true)]
  82. private $introText;
  83. /**
  84. * Mathproblem template
  85. */
  86. #[ORM\JoinColumn(name: 'template_id', referencedColumnName: 'id')]
  87. #[ORM\ManyToOne(targetEntity: \App\Entity\Core\TemplateMathproblem::class, inversedBy: 'mathproblems')]
  88. private $template;
  89. /**
  90. * Relative path to the GeoGebra problem script file.
  91. *
  92. * @var string
  93. */
  94. #[ORM\Column(type: 'string', nullable: true)]
  95. private $geogebra;
  96. /**
  97. * 0 - available/published
  98. * 1 - unpublished
  99. * 2 - deleted
  100. *
  101. * @var ?int
  102. */
  103. #[ORM\Column(name: 'status', type: 'integer', nullable: true)]
  104. private $status;
  105. /**
  106. * @var int
  107. */
  108. #[ORM\Column(name: 'subquestion_nr', type: 'smallint', nullable: true)]
  109. private $subquestionNr;
  110. /**
  111. * Flag used to check if there's any next or previous problem
  112. * @var bool
  113. */
  114. private $hasNext;
  115. /**
  116. * Flag used to control if the problem should be searchable in a quiz
  117. * @var bool
  118. */
  119. #[ORM\Column(name: 'allow_in_quiz_or_worksheet', type: 'boolean', nullable: true)]
  120. private $allowInQuizOrWorksheet;
  121. #[ORM\OneToMany(targetEntity: \App\Entity\Core\RelationQuizMathproblem::class, mappedBy: 'mathproblem')]
  122. private Collection $quizzes;
  123. /**
  124. * @var ArrayCollection
  125. */
  126. #[ORM\JoinTable(name: 'problems_appendices')]
  127. #[ORM\JoinColumn(name: 'problem_id', referencedColumnName: 'id')]
  128. #[ORM\InverseJoinColumn(name: 'appendix_id', referencedColumnName: 'id')]
  129. #[ORM\ManyToMany(targetEntity: \App\Entity\Core\Appendix::class, inversedBy: 'problems', cascade: ['persist', 'remove'])]
  130. private $appendices;
  131. #[ORM\JoinColumn(name: 'video_reference', referencedColumnName: 'id', nullable: true)]
  132. #[ORM\OneToOne(targetEntity: \App\Entity\Core\VideoReference::class, inversedBy: 'mathproblem')]
  133. private ?VideoReference $videoReference;
  134. /**
  135. * @var \DateTime
  136. */
  137. #[ORM\Column(name: 'createdAt', type: 'datetime', nullable: false)]
  138. private \DateTime $createdAt;
  139. /**
  140. * 0 = default view
  141. * 1,2,3,... = alternate views, that can vary by problem type
  142. *
  143. * @var int
  144. */
  145. #[ORM\Column(name: 'view_mode', type: 'smallint', options: ['default' => 0])]
  146. private int $viewMode = 0;
  147. public array $excludeKeys = [];
  148. protected bool $useTransform = false;
  149. protected Transform $transform;
  150. #[ORM\OneToOne(targetEntity: \App\Entity\Core\ProblemDnd::class, mappedBy: 'problem')]
  151. private $problemDnd;
  152. static function setTransform(Transform $transform){
  153. self::$transform = $transform;
  154. }
  155. public function __construct()
  156. {
  157. $this->answers = new ArrayCollection();
  158. $this->appendices = new ArrayCollection();
  159. $this->quizzes = new ArrayCollection();
  160. $this->imageReference = null;
  161. $this->videoReference = null;
  162. $this->audioReference = null;
  163. }
  164. /**
  165. * @return int
  166. */
  167. public function getId()
  168. {
  169. return $this->id;
  170. }
  171. /**
  172. * @return string
  173. */
  174. public function getName()
  175. {
  176. return $this->name;
  177. }
  178. public function setName(string $name)
  179. {
  180. $this->name = $name;
  181. }
  182. /**
  183. * @return string
  184. */
  185. public function getDescription()
  186. {
  187. return $this->description ?? '';
  188. }
  189. public function setDescription(string $description)
  190. {
  191. $this->description = $description;
  192. }
  193. public function getAnswers(): Collection
  194. {
  195. return $this->answers;
  196. }
  197. public function getHint(): ?string
  198. {
  199. return $this->hint;
  200. }
  201. public function setHint(?string $hint)
  202. {
  203. $this->hint = $hint;
  204. }
  205. /**
  206. * @return string|null
  207. */
  208. public function getVideoHint(): ?string
  209. {
  210. return $this->videoHint;
  211. }
  212. /**
  213. * @param string|null $videoHint
  214. */
  215. public function setVideoHint(?string $videoHint): void
  216. {
  217. $this->videoHint = $videoHint;
  218. }
  219. public function getSolution(): ?string
  220. {
  221. return $this->solution;
  222. }
  223. public function setSolution(?string $solution)
  224. {
  225. $this->solution = $solution;
  226. }
  227. /**
  228. * @return TemplateMathproblem
  229. */
  230. public function getTemplate()
  231. {
  232. return $this->template;
  233. }
  234. public function setTemplate(TemplateMathproblem $template)
  235. {
  236. $this->template = $template;
  237. }
  238. /**
  239. * @return ?int
  240. */
  241. public function getStatus()
  242. {
  243. return $this->status;
  244. }
  245. /**
  246. * @param ?int $status
  247. */
  248. public function setStatus($status)
  249. {
  250. $this->status = $status;
  251. }
  252. /**
  253. * Status helper function
  254. */
  255. public function isPublished(): bool
  256. {
  257. // Treating null as published since status is nullable, and it would have been treated like this before, though it should not happen.
  258. return $this->status === self::STATUS_PUBLISHED || $this->status === null;
  259. }
  260. /**
  261. * Status helper function
  262. */
  263. public function setPublished(): void
  264. {
  265. $this->status = self::STATUS_PUBLISHED;
  266. }
  267. /**
  268. * Status helper function
  269. */
  270. public function isUnpublished(): bool
  271. {
  272. return $this->status === self::STATUS_UNPUBLISHED;
  273. }
  274. /**
  275. * Status helper function
  276. */
  277. public function setUnpublished(): void
  278. {
  279. $this->status = self::STATUS_UNPUBLISHED;
  280. }
  281. /**
  282. * Status helper function
  283. */
  284. public function isDeleted(): bool
  285. {
  286. return $this->status === self::STATUS_DELETED;
  287. }
  288. /**
  289. * Status helper function
  290. */
  291. public function setDeleted(): void
  292. {
  293. $this->status = self::STATUS_DELETED;
  294. }
  295. /**
  296. * @return int
  297. */
  298. public function getSubquestionNr()
  299. {
  300. return $this->subquestionNr;
  301. }
  302. /**
  303. * @param int $subquestionNr
  304. *
  305. * Specify if the math problem is part of a bigger question with several subquestions
  306. *
  307. */
  308. public function setSubquestionNr($subquestionNr)
  309. {
  310. $this->subquestionNr = $subquestionNr;
  311. }
  312. public function getGeogebra(): ?string
  313. {
  314. return $this->geogebra;
  315. }
  316. /**
  317. * @return string|null
  318. */
  319. public function getFreeText(): ?string
  320. {
  321. return $this->freeText;
  322. }
  323. /**
  324. * @param string|null $freeText
  325. */
  326. public function setFreeText(?string $freeText): void
  327. {
  328. $this->freeText = $freeText;
  329. }
  330. /**
  331. * @return string|null
  332. */
  333. public function getIntroText(): ?string
  334. {
  335. return $this->introText;
  336. }
  337. /**
  338. * @param string|null $introText
  339. */
  340. public function setIntroText(?string $introText): void
  341. {
  342. $this->introText = $introText;
  343. }
  344. public function getCorrectAnswer(): ?Answer
  345. {
  346. $correctAnswer = null;
  347. foreach ($this->answers as $answer) {
  348. if ($answer->getIsCorrect()) {
  349. $correctAnswer = $answer;
  350. break;
  351. }
  352. }
  353. return $correctAnswer;
  354. }
  355. public function getWrongAnswer(): ?Answer
  356. {
  357. $wrongAnswer = null;
  358. foreach ($this->answers as $answer) {
  359. if (!$answer->getIsCorrect()) {
  360. $wrongAnswer = $answer;
  361. break;
  362. }
  363. }
  364. return $wrongAnswer;
  365. }
  366. /**
  367. * @return array|null
  368. */
  369. public function getCorrectAnswers(): ?array
  370. {
  371. // TODO - Jon's part, try to optimize for reusing
  372. $answerString = $this->getCorrectAnswer()->getAnswerText();
  373. $type = $this->getTemplate()->getType();
  374. if ($type === TemplateMathproblem::TYPE_DROPDOWN) {
  375. // Get correct answers dynamically
  376. $stringArray = explode("#", $answerString);
  377. $correctAnswers = [];
  378. $index = 0;
  379. foreach ($stringArray as $str) {
  380. // if string contains '|', it contains dropdown options
  381. if (!(strpos($str, '|') === false)) {
  382. $dropdownChoices = explode("|", $str);
  383. $correctAnswers[$index] = $dropdownChoices[0];
  384. foreach ($dropdownChoices as $choice){
  385. if(preg_match("#^\*#", $choice) || preg_match("#\*$#", $choice)){
  386. $choice = preg_replace("#^\*#", "", $choice);
  387. $choice = preg_replace("#\*$#", "", $choice);
  388. $correctAnswers[$index] = $choice;
  389. }
  390. }
  391. $index++;
  392. }
  393. }
  394. return $correctAnswers;
  395. } elseif ($type === TemplateMathproblem::TYPE_WORD_OPTIONS) {
  396. $stringArray = explode("#", $answerString);
  397. $correctAnswers = [];
  398. for ($i = 1; $i < count($stringArray); $i += 2) {
  399. $str = $stringArray[$i];
  400. if (in_array($str, ['<br>', '<br/>'])) {
  401. continue;
  402. }
  403. if (strpos($str, '|') === false) {
  404. $correctAnswers[] = 'default';
  405. } else {
  406. $correctAnswers[] = explode('|', $str)[1];
  407. }
  408. }
  409. return $correctAnswers;
  410. } else {
  411. // TODO: perhaps make this possible for other template types too.
  412. return null;
  413. }
  414. }
  415. /**
  416. * @return bool
  417. */
  418. public function getHasNext(){
  419. return $this->hasNext;
  420. }
  421. public function setHasNext($hasNext){
  422. $this->hasNext = $hasNext;
  423. }
  424. public function getAppendices(): Collection
  425. {
  426. return $this->appendices;
  427. }
  428. public function setAppendices(ArrayCollection $appendices): void
  429. {
  430. $this->appendices = $appendices;
  431. }
  432. public function getVideo(): ?Video
  433. {
  434. return $this->video;
  435. }
  436. function jsonSerialize(): mixed
  437. {
  438. // Initialize the $video property if it's not already initialized.
  439. $this->video ??= null;
  440. $data = [
  441. "id" => $this->getId(),
  442. "name" => $this->getName(),
  443. "description" => $this->getDescription(),
  444. "introText" => $this->getIntroText(),
  445. "freeText" => $this->getFreeText(),
  446. "hint" => $this->getHint(),
  447. "correctAnswer" => $this->getCorrectAnswer(),
  448. "template" => $this->getTemplate(),
  449. "status" => $this->getStatus(),
  450. "subquestionNr" => $this->getSubquestionNr(),
  451. "georgeBra" => $this->getGeogebra(),
  452. "solution" => $this->getSolution(),
  453. "image" => $this->getImagePath(),
  454. "appendices" => $this->getAppendices()->toArray(),
  455. "hasNext" => $this->getHasNext(),
  456. "video" => $this->getVideo(),
  457. "wrongAnswer" => $this->getWrongAnswer(),
  458. "topicDisplayName" => $this->getTemplate()->getTopic()->getDisplayName(),
  459. "options" => $this->getCorrectAnswer() && $this->getCorrectAnswer()->getAnswerText() ? explode("#", $this->getCorrectAnswer()->getAnswerText()) : [],
  460. "viewMode" => $this->getViewMode(),
  461. ];
  462. for ($i=0, $count = count($this->excludeKeys); $i < $count; $i++){
  463. if(isset( $data[ $this->excludeKeys[$i] ] )){
  464. unset( $data[ $this->excludeKeys[$i] ] );
  465. }
  466. }
  467. $answers = $this->getAnswers()->toArray();
  468. if($this->getTemplate()->getShouldShuffle()){
  469. shuffle($answers);
  470. }
  471. $data['answers'] = $answers;
  472. return $data;
  473. }
  474. /**
  475. * @param array $keys
  476. * @return $this
  477. */
  478. function exclude(array $keys): self{
  479. $this->excludeKeys = $keys;
  480. return $this;
  481. }
  482. /**
  483. * @param bool $useTransform
  484. * @return $this
  485. */
  486. function setUseTransform(bool $useTransform = true) : self{
  487. $this->useTransform = $useTransform;
  488. return $this;
  489. }
  490. public function getAllowInQuizOrWorksheet(): ?bool
  491. {
  492. return $this->allowInQuizOrWorksheet;
  493. }
  494. public function setAllowInQuizOrWorksheet(?bool $allowInQuizOrWorksheet): void
  495. {
  496. $this->allowInQuizOrWorksheet = $allowInQuizOrWorksheet;
  497. }
  498. public function getViewMode(): int
  499. {
  500. return $this->viewMode;
  501. }
  502. public function getDifficulty() {
  503. return $this->template->getDifficulty();
  504. }
  505. public function getCorrectAnswerText() {
  506. return $this->getCorrectAnswer()?->getAnswerText();
  507. }
  508. /*
  509. * Used for EasyAdmin problem form
  510. *
  511. * */
  512. public function getTemplateId() {
  513. return $this->getTemplate()->getId();
  514. }
  515. public function getType() {
  516. return $this?->getTemplate()?->getType();
  517. }
  518. public function setType(int $type) {
  519. $this->getTemplate()->setType($type);
  520. }
  521. public function getQuizzes(): Collection
  522. {
  523. $quizzes = new ArrayCollection();
  524. foreach ($this->quizzes as $quiz) {
  525. $quizzes->add($quiz->getQuiz());
  526. }
  527. return $quizzes;
  528. }
  529. public function getQuizId(): ?int {
  530. $quizzes = $this->getQuizzes();
  531. return $quizzes[0]?->getId();
  532. }
  533. public function getPublicationName(): string {
  534. $quizzes = $this->getQuizzes();
  535. return $quizzes[0]?->getPublication()?->getName();
  536. }
  537. public function isValidated(): bool
  538. {
  539. return $this->validated ?? false;
  540. }
  541. public function setValidated(bool $validated): void
  542. {
  543. $this->validated = $validated;
  544. }
  545. public function getProblemText(): string {
  546. return '';
  547. }
  548. public function setProblemText(string $text) {
  549. }
  550. public function __toString(): string
  551. {
  552. return $this->name;
  553. }
  554. public function setViewMode(int $viewMode): void
  555. {
  556. $this->viewMode = $viewMode;
  557. }
  558. public function getSnippet(): string
  559. {
  560. // Placeholder for EasyAdmin, value will be overwritten, so it does not matter what is returned.
  561. return '';
  562. }
  563. /**
  564. * @return ?ProblemDnd
  565. */
  566. public function getProblemDnd()
  567. {
  568. return $this->problemDnd;
  569. }
  570. public function getVideoReference(): ?VideoReference
  571. {
  572. return $this->videoReference;
  573. }
  574. public function setVideoReference(?VideoReference $videoReference): void
  575. {
  576. $this->videoReference = $videoReference;
  577. }
  578. public function isValidatedAndHasMediaFiles(): bool
  579. {
  580. if (!$this->isValidated()) {
  581. return false;
  582. }
  583. // detect if problem has media files
  584. if ($this->mediaCouplingExistButWithoutFile(ImageReference::class)) {
  585. return false;
  586. }
  587. if ($this->mediaCouplingExistButWithoutFile(AudioReference::class)) {
  588. return false;
  589. }
  590. // detect if answers have media files
  591. foreach ($this->getAnswers() as $answer) {
  592. if ($answer->mediaCouplingExistButWithoutFile(ImageReference::class)) {
  593. return false;
  594. }
  595. if ($answer->mediaCouplingExistButWithoutFile(AudioReference::class)) {
  596. return false;
  597. }
  598. }
  599. // detect if containers have media files if dnd
  600. if ($this->getType() === 4) {
  601. $bins = $this->getProblemDnd()->getBins();
  602. foreach ($bins as $bin) {
  603. if ($bin->mediaCouplingExistButWithoutFile(ImageReference::class)) {
  604. return false;
  605. }
  606. if ($bin->mediaCouplingExistButWithoutFile(AudioReference::class)) {
  607. return false;
  608. }
  609. }
  610. }
  611. return $this->isValidated();
  612. }
  613. public function getActiveBugReports(): array
  614. {
  615. $bugReports = $this->bugReports;
  616. $activeBugReports = [];
  617. foreach ($bugReports as $bugReport) {
  618. if ($bugReport->getDateResolved() === null) {
  619. $activeBugReports[] = $bugReport->getId();
  620. }
  621. }
  622. return $activeBugReports;
  623. }
  624. public function getCreatedAt(): \DateTime
  625. {
  626. return $this->createdAt;
  627. }
  628. public function setCreatedAt(\DateTime $createdAt): void
  629. {
  630. $this->createdAt = $createdAt;
  631. }
  632. public function getTopic(): ?string
  633. {
  634. return $this->getTemplate()?->getTopic()?->getName();
  635. }
  636. public function setTopic(Topic $topic)
  637. {
  638. $this->getTemplate()?->setTopic($topic);
  639. }
  640. public function getCategory(): ?string
  641. {
  642. return $this->getTemplate()?->getTopic()?->getCategory()?->getName();
  643. }
  644. public function getImage(){
  645. return $this->getImagePath();
  646. }
  647. public function getCmsImagePath(): string
  648. {
  649. if (!$this->imageReference) {
  650. return 'Mangler';
  651. }
  652. if (!$this->imageReference->getFilePath()) {
  653. return 'Mangler fil';
  654. }
  655. return $this->getImagePath();
  656. }
  657. public function getCmsAudioPath(): string
  658. {
  659. if (!$this->audioReference) {
  660. return 'Mangler';
  661. }
  662. if (!$this->audioReference->getFilePath()) {
  663. return 'Mangler fil';
  664. }
  665. return $this->getAudioPath();
  666. }
  667. }