closedquestion-8.x-3.x-dev/src/Question/CqQuestionArrow.php
src/Question/CqQuestionArrow.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 CqQuestionArrow.
*
* Implementation of the CqQuestionArrow question type.
* A CqQuestionArrow question presents the student with an image
* and the assignment to draw arrows in that image.
*
* @package Drupal\closedquestion\Question
*/
class CqQuestionArrow extends CqQuestionAbstract {
/**
* HTML containing the question-text.
*
* @var string
*/
private $text;
/**
* The base-name used for form elements that need to be accessed by js.
*
* @var string
*/
private $formElementName;
/**
* The url of the image to use as background for the hotspot question.
*
* @var string
*/
private $matchImgUrl;
/**
* The width of the background image.
*
* @var int
*/
private $matchImgWidth;
/**
* The height of the background image.
*
* @var int
*/
private $matchImgHeight;
/**
* Maximum number of items the student is allowed to point out.
*
* Defaults to 1;
*
* @var int
*/
private $maxChoices = 1;
/**
* The list of hotspots in the image.
*
* @var \Drupal\closedquestion\Question\Mapping\CqHotspotInterface[]
*/
private $hotspots = array();
/**
* The mappings to check the student's answer against.
*
* @var \Drupal\closedquestion\Question\Mapping\CqAbstractMapping[]
*/
private $mappings = array();
/**
* The mappings that have the correct flag set and matched the current answer.
*
* @var \Drupal\closedquestion\Question\Mapping\CqAbstractMapping[]
*/
private $matchedCorrectMappings = array();
/**
* The mappings that matched the current answer.
*
* @var \Drupal\closedquestion\Question\Mapping\CqAbstractMapping[]
*/
private $matchedMappings = array();
/**
* List of feedback items to use as general hints.
*
* @var \Drupal\closedquestion\Question\Mapping\CqFeedback[]
*/
private $hints = array();
/**
* Constructs a ChemReaction 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 = 'cq_arrow_question' . $this->closedQuestion->id() . '_';
}
/**
* Implements CqQuestionAbstract::getOutput()
*/
public function getOutput() {
$this->initialise();
$retval = $this->formBuilder->getForm('\Drupal\closedquestion\Form\QuestionForm', $this->getClosedQuestion());
$retval['#prefix'] = $this->prefix;
$retval['#suffix'] = $this->postfix;
return $retval;
}
/**
* Overrides CqQuestionAbstract::getDraggables()
*/
public function getDraggables() {
return $this->draggables;
}
/**
* Overrides CqQuestionAbstract::getHotspots()
*/
public function getHotspots() {
return $this->hotspots;
}
/**
* Overrides CqQuestionAbstract::loadXml()
*/
public function loadXml(\DOMNode $dom) {
parent::loadXml($dom);
$this->hotspots = array();
$this->mappings = array();
$this->hints = array();
$this->linestyle = $dom->getAttribute('linestyle') == '' ? 'curved' : $dom->getAttribute('linestyle');
$this->linenumbering = $dom->getAttribute('linenumbering') !== 'no' ? TRUE : FALSE;
$this->startarrow = $dom->getAttribute('startarrow') !== 'yes' ? FALSE : TRUE;
$this->endarrow = $dom->getAttribute('endarrow') !== 'no' ? TRUE : FALSE;
foreach ($dom->childNodes as $node) {
$name = mb_strtolower($node->nodeName);
switch ($name) {
case 'text':
$this->text = $this->xmlLib->getTextContent($node, $this);
break;
case 'matchimg':
$this->matchImgUrl = $this->getUrlFromMediaTag($node->getAttribute('src'));
$this->matchImgHeight = $node->getAttribute('height');
$this->matchImgWidth = $node->getAttribute('width');
$this->maxChoices = $node->getAttribute('maxChoices');
foreach ($node->childNodes as $child) {
switch (mb_strtolower($child->nodeName)) {
case 'hotspot':
$hotspot = cq_Hotspot_from_xml($child, $this);
if (is_object($hotspot)) {
if (isset($this->hotspots[$hotspot->getIdentifier()])) {
$this->messenger->addMessage(t('Hotspot identifier %identifier used more than once!', array('%identifier' => $hotspot->getIdentifier())), 'warning');
}
$this->hotspots[$hotspot->getIdentifier()] = $hotspot;
}
break;
}
}
break;
case 'mapping':
$map = new CqMapping();
$map->generateFromNode($node, $this);
$this->mappings[] = $map;
break;
case 'hint':
$this->hints[] = CqFeedback::newCqFeedback($node, $this);
break;
default:
if (!in_array($name, $this->knownElements)) {
$this->messenger->addMessage(t('Unknown node: @nodename', array('@nodename' => $node->nodeName)));
}
break;
}
}
}
/**
* Returns user answer.
*
* This function should be used in stead of calling
* $this->getUserAnswer()->getAnswer() directly,
* as the answer needs some basic filtering.
*
* @return string
* Filtered answer.
*/
public function getUserAnswerAsString() {
// Remove # (used to store arrowInversion, but not for feedback).
return str_replace('*', '', $this->getUserAnswer()->getAnswer());
}
/**
* 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 general hints, only of the answer is not correct.
if (!$this->isCorrect()) {
foreach ($this->hints as $fb) {
if ($fb->inRange($tries)) {
$feedback[] = $fb;
}
}
}
// 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.
return array_merge($feedback, $this->fireGetExtraFeedbackItems($this, $tries));
}
/**
* Implements CqQuestionAbstract::checkCorrect()
*/
public function checkCorrect() {
$this->matchedMappings = array();
$this->matchedCorrectMappings = array();
$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;
}
/**
* Implements CqQuestionAbstract::getForm()
*/
public function getForm($formState) {
$answer = $this->getUserAnswerAsString();
$mapName = $this->formElementName . 'map';
// The question part is themed.
$form['question']['#theme'] = 'closedquestion_question_arrow';
$form['question']['questionText'] = array(
'#type' => 'item',
'#markup' => $this->text,
);
// The data needed by the theme function.
$data = array();
$data['elementname'] = $this->formElementName;
$data['mapname'] = $mapName;
$data['image'] = array(
'height' => (int) $this->matchImgHeight,
'width' => (int) $this->matchImgWidth,
'url' => $this->matchImgUrl,
);
$data['linestyle'] = $this->linestyle;
$data['linenumbering'] = $this->linenumbering;
$data['startarrow'] = $this->startarrow;
$data['endarrow'] = $this->endarrow;
// Handle the hotspots.
foreach ($this->hotspots as $hotspot) {
$description = $hotspot->getDescription();
$termId = $this->formElementName . 'term_' . $hotspot->getIdentifier();
$data['hotspots'][$hotspot->getIdentifier()] = array(
'termid' => $termId,
'maphtml' => $hotspot->getMapHtml(),
'mapdata' => $hotspot->getMapData(),
'description' => $description,
);
}
$form['question']['data'] = array('#type' => 'value', '#value' => $data);
// Other elements are not themed by default.
// This element will be filled by Javascript to contain the answer.
$form[$this->formElementName . 'answer'] = array(
'#type' => 'hidden',
'#default_value' => $answer,
'#input' => TRUE,
);
// 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;
}
/**
* Gets the answer.
*
* Get the answer the student has given for the given target box, or the full
* answer string if the given identifier is not a target box.
*
* @param string $identifier
* The target-box number to fetch the answer for.
*
* @return string
* The answer.
*/
public function getAnswerForChoice($identifier) {
$answer = $this->getUserAnswerAsString();
if (is_numeric($identifier)) {
$part = (int) $identifier;
$start = strpos($answer, $part);
$end = strpos($answer, $part + 1, $start);
$length = max(0, max(mb_strlen($answer), $end) - $start);
// Not using mb_substr since we use a strpos generated indexes.
$answer = substr($answer, $start, $length);
}
if ($this->currentUser->hasPermission(CLOSEDQUESTION_RIGHT_CREATE) && !isset($this->messsageSet)) {
$this->messenger->addMessage(t('Current answer=%a (Teacher only message)', array('%a' => $answer)));
$this->messsageSet = TRUE;
}
return $answer;
}
/**
* Implements CqQuestionAbstract::submitAnswer()
*/
public function submitAnswer($form, FormStateInterface $form_state) {
$newAnswer = $form_state->getValue($this->formElementName . 'answer');
$this->userAnswer->setAnswer($newAnswer);
$correct = $this->isCorrect(TRUE);
if ($this->userAnswer->answerHasChanged()) {
if (!$correct) {
$this->userAnswer->increaseTries();
}
$this->userAnswer->store();
}
}
/**
* Implements CqQuestionAbstract::getAllText()
*/
public function getAllText() {
$this->initialise();
$retval = array();
$retval['text']['#markup'] = $this->text;
// Hints.
$retval['hints'] = array(
'#theme' => 'closedquestion_feedback_list',
'#extended' => TRUE,
);
foreach ($this->hints as $fbitem) {
$retval['hints']['items'][] = $fbitem->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;
}
}
