src/Entity/Core/Mathproblem.php line 17

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