quiz-6.0.0-alpha4/modules/quiz_multichoice/src/Plugin/quiz/QuizQuestion/MultichoiceQuestion.php
modules/quiz_multichoice/src/Plugin/quiz/QuizQuestion/MultichoiceQuestion.php
<?php
namespace Drupal\quiz_multichoice\Plugin\quiz\QuizQuestion;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\quiz\Attribute\QuizQuestion as QuizQuestionAttribute;
use Drupal\quiz\Entity\QuizQuestion;
use Drupal\quiz\Entity\QuizResultAnswer;
use function check_markup;
/**
* Multichoice Question.
*/
#[QuizQuestionAttribute(
id: 'multichoice',
label: new TranslatableMarkup('Multiple choice question'),
handlers: ['response' => MultichoiceResponse::class],
)]
class MultichoiceQuestion extends QuizQuestion {
use StringTranslationTrait;
use MessengerTrait;
/**
* {@inheritdoc}
*/
public function save(): ?int {
// After we save the question, we forgive some possible user errors on the
// alternatives.
$saved = parent::save();
$this->forgive();
$this->warn();
return $saved;
}
/**
* Forgive some possible logical flaws in the user input.
*/
private function forgive(): void {
$config = \Drupal::config('quiz_multichoice.settings');
if ($this->get('choice_multi')->value) {
$alternatives = $this->get('alternatives')->referencedEntities();
foreach ($alternatives as $alternative) {
// If the scoring data doesn't make sense, use the data from the
// "correct" checkbox to set the score data.
if ($alternative->get('multichoice_score_chosen')->value == $alternative->get('multichoice_score_not_chosen')->value || !is_numeric($alternative->get('multichoice_score_chosen')->value) || !is_numeric($alternative->get('multichoice_score_not_chosen')->value)) {
if (!empty($alternative->get('multichoice_correct')->value)) {
$alternative->set('multichoice_score_chosen', 1);
$alternative->set('multichoice_score_not_chosen', 0);
}
else {
if ($config->get('scoring') == 0) {
$alternative->set('multichoice_score_chosen', -1);
$alternative->set('multichoice_score_not_chosen', 0);
}
elseif ($config->get('scoring') == 1) {
$alternative->set('multichoice_score_chosen', 0);
$alternative->set('multichoice_score_not_chosen', 1);
}
}
}
$alternative->save();
}
}
else {
// For questions with one, and only one, correct answer, there will be
// no points awarded for alternatives not chosen.
$alternatives = $this->get('alternatives')
->referencedEntities();
foreach ($alternatives as $alternative) {
if ($alternative->get('multichoice_correct')->value == 1 && $alternative->get('multichoice_score_chosen')->value <= 0) {
$alternative->set('multichoice_score_chosen', 1);
}
if ($alternative->get('multichoice_correct')->value == 0) {
$alternative->set('multichoice_score_not_chosen', 0);
}
$alternative->save();
}
}
}
/**
* Warn the user about possible user errors.
*/
private function warn(): void {
// Count the number of correct answers.
$num_corrects = 0;
$alternatives = $this->get('alternatives')
->referencedEntities();
foreach ($alternatives as $alternative) {
if ($alternative->get('multichoice_score_chosen')->value > $alternative->get('multichoice_score_not_chosen')->value) {
$num_corrects++;
}
}
if ($num_corrects == 1 && $this->get('choice_multi')->value == 1 || $num_corrects > 1 && $this->get('choice_multi')->value == 0) {
$go_back = Url::fromRoute('entity.quiz_question.canonical', ['quiz_question' => $this->id()])->toString();
if ($num_corrects == 1) {
$this->messenger()->addWarning(
$this->t("Your question allows multiple answers. Only one of the alternatives have been marked as correct. If this wasn't intended please <a href=\"@go_back\">go back</a> and correct it.", ['@go_back' => $go_back]), 'warning');
}
else {
$this->messenger()->addWarning(
$this->t("Your question doesn't allow multiple answers. More than one of the alternatives have been marked as correct. If this wasn't intended please <a href=\"@go_back\">go back</a> and correct it.", ['@go_back' => $go_back]), 'warning');
}
}
}
/**
* {@inheritdoc}
*/
public function getAnsweringForm(FormStateInterface $form_state, QuizResultAnswer $quizQuestionResultAnswer): array {
$element = parent::getAnsweringForm($form_state, $quizQuestionResultAnswer);
$alternatives = [];
foreach ($this->get('alternatives')->referencedEntities() as $alternative) {
/** @var \Drupal\paragraphs\Entity\Paragraph $alternative */
$uuid = $alternative->get('uuid')->getString();
$alternatives[$uuid] = $alternative;
}
// Build options list.
$element['user_answer'] = [
'#type' => 'tableselect',
'#header' => ['answer' => $this->t('Answer')],
'#js_select' => FALSE,
'#multiple' => $this->get('choice_multi')->getString(),
];
// @todo see https://www.drupal.org/project/drupal/issues/2986517
// There is some way to label the elements.
foreach ($alternatives as $alternative) {
$vid = $alternative->getRevisionId();
$multichoice_answer = $alternative->get('multichoice_answer')->getValue()[0];
$answer_markup = check_markup($multichoice_answer['value'], $multichoice_answer['format']);
$element['user_answer']['#options'][$vid]['title']['data']['#title'] = $answer_markup;
$element['user_answer']['#options'][$vid]['answer'] = $answer_markup;
}
if ($this->get('choice_random')->getString()) {
// We save the choice order so that the order will be the same in the
// answer report.
$element['choice_order'] = [
'#type' => 'hidden',
'#value' => implode(',', $this->shuffle($element['user_answer']['#options'])),
];
}
if ($quizQuestionResultAnswer->isAnswered()) {
$choices = $quizQuestionResultAnswer->getResponse();
if ($this->get('choice_multi')->getString()) {
foreach ($choices as $choice) {
$element['user_answer']['#default_value'][$choice] = TRUE;
}
}
else {
$element['user_answer']['#default_value'] = reset($choices);
}
}
return $element;
}
/**
* Custom shuffle function.
*
* It keeps the array key - value relationship intact.
*
* @param array $array
* Array to be shuffled.
*
* @return array
* The shuffled array.
*/
private function shuffle(array &$array): array {
$newArray = [];
$toReturn = array_keys($array);
shuffle($toReturn);
foreach ($toReturn as $key) {
$newArray[$key] = $array[$key];
}
$array = $newArray;
return $toReturn;
}
/**
* {@inheritdoc}
*/
public function getMaximumScore(): int {
if ($this->get('choice_boolean')->getString()) {
// Simple scoring - can only be worth 1 point.
return 1;
}
$maxes = [0];
foreach ($this->get('alternatives')->referencedEntities() as $alternative) {
// "Not chosen" could have a positive point amount.
$maxes[] = max($alternative->get('multichoice_score_chosen')->getString(), $alternative->get('multichoice_score_not_chosen')->getString());
}
if ($this->get('choice_multi')->getString() && is_array($maxes)) {
// For multiple answers, return the maximum possible points of all
// positively pointed answers.
return array_sum($maxes);
}
else {
// For a single answer, return the highest pointed amount.
return max($maxes);
}
}
/**
* {@inheritdoc}
*/
public static function getAnsweringFormValidate(array &$element, FormStateInterface $form_state): void {
$mcq = $element['#quiz_result_answer']->getQuizQuestion();
if (!$mcq->get('choice_multi')->getString() && empty($element['user_answer']['#value'])) {
$form_state->setError($element, (t('You must provide an answer.')));
}
parent::getAnsweringFormValidate($element, $form_state);
}
}
