closedquestion-8.x-3.x-dev/src/Question/CqQuestionCheck.php
src/Question/CqQuestionCheck.php
<?php
namespace Drupal\closedquestion\Question;
use Drupal\closedquestion\Entity\ClosedQuestionInterface;
use Drupal\closedquestion\Question\Mapping\CqFeedback;
use Drupal\closedquestion\Question\Mapping\CqMapping;
use Drupal\Core\Form\FormStateInterface;
/**
* Class CqQuestionCheck.
*
* Implementation of a Multiple answer question (multiple choice with more than
* one selectable)
*
* @package Drupal\closedquestion\Question
*/
class CqQuestionCheck extends CqQuestionAbstract {
/**
* HTML containing the question-text.
*
* @var string
*/
protected $text;
/**
* The base-name used for form elements that need to be accessed by js.
*
* @var string
*/
protected $formElementName;
/**
* Prompt do display directly above the options.
*
* @var string
*/
protected $prompt = '';
/**
* The list of options for the student to choose from.
*
* @var \Drupal\closedquestion\Question\CqOption[]
* Array of options.
*/
protected $options = array();
/**
* List of feedback items to use as general hints.
*
* @var \Drupal\closedquestion\Question\Mapping\CqFeedback[]
* Array of feedback items.
*/
protected $hints = array();
/**
* Feedback mappings.
*
* @var \Drupal\closedquestion\Question\Mapping\CqMapping[]
* Array of mappings.
*/
protected $mappings = array();
/**
* Mappings that have the "correct" flag set and matched the current answer.
*
* @var \Drupal\closedquestion\Question\Mapping\CqMapping[]
* Array of mappings.
*/
protected $matchedCorrectMappings = array();
/**
* Mappings that mached the current answer.
*
* @var \Drupal\closedquestion\Question\Mapping\CqMapping[]
* Array of mappings.
*/
protected $matchedMappings = array();
/**
* The feedback to show when the answer is answered correctly.
*
* @var \Drupal\closedquestion\Question\Mapping\CqFeedback
*/
protected $correctFeeback;
/**
* Show feedback in-line?
*
* 1: yes, any other value: no;
*
* @var int
*/
protected $inlineFeedback = 0;
/**
* The answer in string-format.
*
* @var string
*/
protected $answerString = NULL;
/**
* Constructs a check question object.
*
* @param CqUserAnswerInterface $userAnswer
* The CqUserAnswerInterface to use for storing the student's answer.
* @param \Drupal\closedquestion\Entity\ClosedQuestionInterface $closedQuestion
* Closed question entity.
*/
public function __construct(CqUserAnswerInterface $userAnswer, ClosedQuestionInterface $closedQuestion) {
parent::__construct();
$this->userAnswer = $userAnswer;
$this->closedQuestion = $closedQuestion;
$this->formElementName = 'xq_check_question' . $this->closedQuestion->id() . '_';
$this->prompt = t('Pick any number');
}
/**
* Implements CqQuestionAbstract::getOutput()
*/
public function getOutput() {
$this->initialise();
$retval = $this->formBuilder->getForm('\Drupal\closedquestion\Form\QuestionForm', $this->closedQuestion);
$retval['#prefix'] = $this->prefix;
$retval['#suffix'] = $this->postfix;
return $retval;
}
/**
* Get the current options (CqQuestionOption objects) for this question.
*
* @return array
* Array of CqQuestionOption objects.
*/
public function getOptions() {
return $this->options;
}
/**
* Implements CqQuestionAbstract::getFeedbackItems()
*/
public function getFeedbackItems() {
$tries = $this->userAnswer->getTries();
$answer = $this->userAnswer->getAnswer();
$feedback = array();
if ($answer == NULL) {
// If there is no answer, don't check any further.
return $feedback;
}
// The direct option-related feedback.
if (!$this->isCorrect()) {
foreach ($this->hints as $fb) {
if ($fb->inRange($tries)) {
$feedback[] = $fb;
}
}
}
// The option-related feedback. Shown always if feedback is inline, or
// only when the answer is not correct if the feedback is not inline.
if (!$this->isCorrect() || $this->inlineFeedback === 1) {
if ($answer !== NULL) {
foreach ($this->options as $optionNr => $option) {
$optionName = 'o' . $optionNr;
if (isset($answer[$optionName])) {
$feedbacks = $option->getFeedback($tries, $answer[$optionName]);
foreach ($feedbacks as $fb) {
if ($this->inlineFeedback === 1) {
$fb->setBlock($optionName);
}
$feedback[] = $fb;
}
}
}
}
}
// The feedback if the answer is correct.
if ($this->isCorrect()) {
if ($this->correctFeeback != NULL) {
$feedback[] = $this->correctFeeback;
}
}
// The new style mappings.
if ($this->isCorrect()) {
foreach ($this->matchedCorrectMappings as $mapping) {
$feedback = array_merge($feedback, $mapping->getFeedbackItems($tries));
}
}
else {
foreach ($this->matchedMappings as $mapping) {
$feedback = array_merge($feedback, $mapping->getFeedbackItems($tries));
}
}
// Finally, ask external systems if they want to add extra feedback.
$feedback = array_merge($feedback, $this->fireGetExtraFeedbackItems($this, $tries));
return $feedback;
}
/**
* Overrides CqQuestionAbstract::loadXml()
*/
public function loadXml(\DOMNode $dom) {
parent::loadXml($dom);
foreach ($dom->childNodes as $node) {
$name = mb_strtolower($node->nodeName);
switch ($name) {
case 'option':
$this->options[] = new CqOption($node, $this);
break;
case 'text':
$this->text = $this->xmlLib->getTextContent($node, $this);
break;
case 'prompt':
$this->prompt = $this->xmlLib->getTextContent($node, $this);
break;
case 'hint':
$this->hints[] = CqFeedback::newCqFeedback($node, $this);
break;
case 'correct':
$this->correctFeeback = CqFeedback::newCqFeedback($node, $this);
break;
case 'mapping':
// New style mapping, continues at match by default.
$map = new CqMapping();
$map->generateFromNode($node, $this);
if ($node->nodeName == 'sequence') {
$map->setStopIfMatch(TRUE);
}
$this->mappings[] = $map;
break;
default:
if (!in_array($name, $this->knownElements)) {
$this->messenger->addMessage(t('Unknown node: @nodename', array('@nodename' => $node->nodeName)));
}
break;
}
}
$attribs = $dom->attributes;
$item = $attribs->getNamedItem('inlinefeedback');
if ($item !== NULL) {
$this->inlineFeedback = (int) $item->value;
}
}
/**
* Implements CqQuestionAbstract::getForm()
*/
public function getForm($formState) {
$form = parent::getForm($formState);
$answer = $this->userAnswer->getAnswer();
$tries = $this->userAnswer->getTries();
$answered = TRUE;
if ($answer === NULL) {
$answer = array();
$answered = FALSE;
}
$formPos = strpos($this->text, '<formblock/>');
if ($formPos !== FALSE) {
// Not using mb_substr since we use a strpos generated index.
$preForm = substr($this->text, 0, $formPos);
$postForm = substr($this->text, $formPos + 12);
}
else {
$form['questionText'] = array(
'#type' => 'item',
'#markup' => $this->text,
);
}
$optionsFinal = array();
foreach ($this->options as $optionNr => $option) {
$optionName = 'o' . $optionNr;
$optionsFinal[$optionName] = $option->getText();
$description = $option->getDescription();
if (!empty($description)) {
$optionsFinal[$optionName] = '<div class="cqTooltipTarget" title=\'' . rawurlencode($description) . '\' ><div class="cqOptionText">' . $optionsFinal[$optionName] . '</div></div>';
}
if ($this->inlineFeedback === 1) {
$optionsFinal[$optionName] .= '<p><span class="cqFbBlock cqFb-' . $optionName . '" ></span></p>';
}
}
$form['options'] = array(
'#type' => 'checkboxes',
'#title' => $this->prompt,
'#options' => $optionsFinal,
'#default_value' => $answer,
);
if ($formPos !== FALSE) {
$form['options']['#prefix'] = $preForm;
$form['options']['#suffix'] = $postForm;
}
// Insert standard feedback and submit elements.
$wrapper_id = 'cq-feedback-wrapper_' . $this->formElementName;
$this->insertFeedback($form, $wrapper_id);
$this->insertSubmit($form, $wrapper_id);
return $form;
}
/**
* Implements CqQuestionAbstract::checkCorrect()
*/
public function checkCorrect() {
$answer = $this->userAnswer->getAnswer();
$this->matchedMappings = array();
$this->matchedCorrectMappings = array();
if (count($this->mappings) > 0) {
// There are mappings, use those.
$correct = FALSE;
foreach ($this->mappings as $id => $mapping) {
if ($mapping->evaluate()) {
if ($mapping->getCorrect() != 0) {
$correct = TRUE;
$this->matchedCorrectMappings[] = $mapping;
}
else {
$this->matchedMappings[] = $mapping;
}
if ($mapping->stopIfMatch()) {
break;
}
}
unset($mapping);
}
return $correct;
}
else {
// No mappings, use direct option-related checks.
foreach ($this->options as $optionNr => $option) {
$optionName = 'o' . $optionNr;
if ($option->getCorrect() >= 0) {
if ((!isset($answer[$optionName]) || $answer[$optionName] === 0) && $option->getCorrect() > 0) {
return FALSE;
}
if ($answer[$optionName] === $optionName && $option->getCorrect() == 0) {
return FALSE;
}
}
}
return TRUE;
}
}
/**
* Implements CqQuestionAbstract::submitAnswer()
*/
public function submitAnswer($form, FormStateInterface $form_state) {
$this->answerString = NULL;
$this->userAnswer->setAnswer($form_state->getValue('options'));
$correct = $this->isCorrect(TRUE);
if ($this->userAnswer->answerHasChanged()) {
if (!$correct) {
$this->userAnswer->increaseTries();
}
$this->userAnswer->store();
}
}
/**
* Returns the the answer in string form.
*
* @param string $identifier
* Unused in this question type.
*
* @return string
* The answer in string form.
*/
public function getAnswerForChoice($identifier) {
$answer = $this->userAnswer->getAnswer();
if ($this->answerString === NULL) {
$this->answerString = '';
foreach ($this->options as $optionNr => $option) {
$optionName = 'o' . $optionNr;
if (isset($answer[$optionName]) && $answer[$optionName] === $optionName) {
$this->answerString .= $option->getIdentifier();
}
}
if ($this->currentUser->hasPermission(CLOSEDQUESTION_RIGHT_CREATE)) {
$this->messenger->addMessage(t('Current answer=%a (Teacher only message)', array('%a' => $this->answerString)));
}
}
return $this->answerString;
}
/**
* Implements CqQuestionAbstract::getAllText()
*/
public function getAllText() {
$this->initialise();
$retval = array();
$retval['text']['#markup'] = $this->text;
if ($this->correctFeeback) {
$retval['correctFeeback']['#markup'] = $this->correctFeeback->getText();
}
// Hints.
if (count($this->hints) > 0) {
$retval['hints'] = array(
'#theme' => 'closedquestion_feedback_list',
'#extended' => TRUE,
);
foreach ($this->hints as $fbitem) {
$retval['hints']['items'][] = $fbitem->getAllText();
}
}
// Options.
$retval['options'] = array(
'#theme' => 'closedquestion_option_list',
'items' => array(),
'#extended' => TRUE,
);
foreach ($this->options as $option) {
$retval['options']['items'][] = $option->getAllText();
}
// Mappings.
$retval['mappings'] = array(
'#theme' => 'closedquestion_mapping_list',
'items' => array(),
);
foreach ($this->mappings as $mapping) {
$retval['mappings']['items'][] = $mapping->getAllText();
}
$retval['#theme'] = 'closedquestion_question_general_text';
return $retval;
}
}
