closedquestion-8.x-3.x-dev/src/Question/CqQuestionFillblanks.php

src/Question/CqQuestionFillblanks.php
<?php

namespace Drupal\closedquestion\Question;

use Drupal\closedquestion\Entity\ClosedQuestionInterface;
use Drupal\closedquestion\Question\Mapping\CqFeedback;
use Drupal\closedquestion\Question\Mapping\CqInlineOption;
use Drupal\closedquestion\Question\Mapping\CqMapping;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Form\FormStateInterface;

/**
 * Class CqQuestionFillblanks.
 *
 * Implementation of the Fillblanks question type.
 *
 * @package Drupal\closedquestion\Question
 */
class CqQuestionFillblanks extends CqQuestionAbstract {

  /**
   * HTML containing the question-text.
   *
   * @var string
   */
  private $text;

  /**
   * The base-name used for form elements.
   *
   * @var string
   */
  private $formElementName = '';

  /**
   * List of options to use in select boxes.
   *
   * @var \Drupal\closedquestion\Question\Mapping\CqInlineOption[]
   */
  private $inlineOptions = array();

  /**
   * List of boxes for the user to fill in.
   *
   * @var array
   *   Associative array with the choiceId as key and an array as value
   *   describing the choice with the following fields:
   *   - id: string, the id of the field.
   *   - group: string
   *   - name: string, name of the input field in the generated html.
   *   - size: int, value to use for the html size attribute.
   *   - style: string, name of a specific css style to use for this field.
   *   - value: string, current value the student filled in.
   *   - freeform: int
   */
  private $inlineChoices = array();

  /**
   * Grouped options.
   *
   * List containing lists of options to use in select boxes, with the group
   * name as key.
   *
   * @var array
   *   Associative array with the group name as key and array of CqInlineOption
   *   as value.
   */
  private $inlineOptionsByGroup = array();

  /**
   * List of feedback items to use as general hints.
   *
   * @var \Drupal\closedquestion\Question\Mapping\CqFeedback[]
   */
  private $hints = array();

  /**
   * The mappings to check the student's answer against.
   *
   * @var \Drupal\closedquestion\Question\Mapping\CqMapping[]
   */
  private $mappings = array();

  /**
   * The mappings that have the correct flag set and matched the current answer.
   *
   * @var \Drupal\closedquestion\Question\Mapping\CqMapping[]
   */
  private $matchedCorrectMappings = array();

  /**
   * The mappings that matched the current answer.
   *
   * @var \Drupal\closedquestion\Question\Mapping\CqMapping[]
   */
  private $matchedMappings = array();

  /**
   * Indicates that answer has already been parsed.
   *
   * Keep track of wether the answer has been parsed into variables yet.
   * This will only be true if createVariables() is called with a math
   * environment present.
   *
   * @var bool
   */
  private $variablesParsed = FALSE;

  /**
   * Constructs a Fill-blanks 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_fillblanks_question' . $this->closedQuestion->id() . '_';
    parent::registerTag('inlinechoice');
  }

  /**
   * 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 answer(s) the student has given for the given hotspot(s).
   *
   * @param string $pattern
   *   A regular expression that is mached against all hotspot id's.
   *
   * @return mixed
   *   array: array of strings of the answers of the hotspots that matched.
   *   string: the answer if only one hotspot mached, or an empty string
   *   if no id matched.
   */
  public function getAnswerForChoice($pattern) {
    if (mb_substr($pattern, 0, 1) != '/') {
      $pattern = '/' . $pattern . '/';
    }
    $answer = $this->userAnswer->getAnswer();
    $retarr = array();

    if (is_array($answer)) {
      foreach ($answer as $id => $value) {
        if (preg_match($pattern, $id)) {
          $retarr[$id] = $value;
        }
      }
    }
    if (count($retarr) > 1) {
      return $retarr;
    }
    elseif (count($retarr) == 1) {
      return reset($retarr);
    }
    return '';
  }

  /**
   * Overrides CqQuestionAbstract::loadXml()
   */
  public function loadXml(\DOMNode $dom) {
    parent::loadXml($dom);


    $textNode = NULL;
    foreach ($dom->childNodes as $node) {
      $name = mb_strtolower($node->nodeName);
      switch ($name) {
        case 'inlineoption':
          $option = new CqInlineOption($node, $this);
          if (isset($this->inlineOptions[$option->getIdentifier()])) {
            $this->messenger->addMessage(t('Inlineoption identifier %identifier used more than once!', array('%identifier' => $option->getIdentifier())), 'warning');
          }
          $this->inlineOptions[$option->getIdentifier()] = $option;
          $this->inlineOptionsByGroup[$option->getGroup()][$option->getIdentifier()] = $option;
          break;

        case 'mapping':
          $map = new CqMapping();
          $map->generateFromNode($node, $this);
          $this->mappings[] = $map;
          break;

        case 'text':
          $textNode = $node;
          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;
      }
    }
    if ($textNode != NULL) {
      $this->text = $this->xmlLib->getTextContent($textNode, $this);
      $this->addVariablesFromQuestionText();
    }
  }

  /**
   * Implements CqQuestionAbstract::checkCorrect()
   */
  public function checkCorrect() {
    $this->matchedCorrectMappings = array();
    $this->matchedMappings = array();
    $correct = FALSE;

    $this->createVariables();

    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;
  }

  /**
   * Overrides CqQuestionAbstract::evaluateMath()
   *
   * Checks if the answer(s) have been parsed into variables yet, and does so
   * if not.
   */
  public function evaluateMath($expression) {
    if (!$this->variablesParsed) {
      if (!$this->hasMath()) {
        // There is no math environment yet, make parent parse an empty
        // expression so it makes a math environment.
        parent::evaluateMath("");
      }
      $this->createVariables();
    }
    return parent::evaluateMath($expression);
  }

  /**
   * Put all numeric answers into MathExpression variables.
   *
   * Only for those answers that have an id that is a valid MathExpression
   * variable name.
   */
  private function createVariables() {
    if ($this->hasMath()) {
      $this->variablesParsed = TRUE;
      $answer = $this->userAnswer->getAnswer();
      foreach ($this->inlineChoices as $id => $option) {
        if (preg_match("/^[a-zA-Z]\w*$/", $id)) {
          if (is_array($answer) && isset($answer[$id]) && is_numeric($answer[$id])) {
            $expression = $id . '=' . $answer[$id];
            $result = $this->evaluateMath($expression);
          }
          else {
            $expression = $id . '=0';
            $result = $this->evaluateMath($expression);
          }
        }
      }
    }
  }

  /**
   * Adds inlineChoices found in question text to evalmath variables array.
   */
  private function addVariablesFromQuestionText() {
    if (is_array($this->inlineChoices)) {
      $evalMathVars = $this->evalMath->getVars();
      foreach ($this->inlineChoices as $inlineChoiceId => $inlinceChoiceVars) {
        $evalMathVars[$inlineChoiceId] = $inlinceChoiceVars['#value'];
      }
      $this->evalMath->setVars($evalMathVars);
    }
  }

  /**
   * 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;
    }

    if ($this->isCorrect()) {
      foreach ($this->matchedCorrectMappings as $mapping) {
        $feedback = array_merge($feedback, $mapping->getFeedbackItems($tries));
      }
    }
    else {
      foreach ($this->hints as $fb) {
        if ($fb->inRange($tries)) {
          $feedback[] = $fb;
        }
      }
      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;
  }

  /**
   * Implements CqQuestionAbstract::submitAnswer()
   */
  public function submitAnswer($form, FormStateInterface $form_state) {
    $this->userAnswer->setAnswer($form_state->getValue($this->formElementName));
    $correct = $this->isCorrect(TRUE);
    if ($this->userAnswer->answerHasChanged()) {
      if (!$correct) {
        $this->userAnswer->increaseTries();
      }
      $this->userAnswer->store();
    }
  }

  /**
   * Implements CqQuestionAbstract::getOutput()
   *
   * The form of the Fillblanks question is too simple to need custom theming,
   * there is no additional html needed.
   */
  public function getForm($formState) {
    $form = parent::getForm($formState);
    $form['questionText'] = array(
      '#type' => 'item',
      '#markup' => $this->text,
    );

    // We have to tell the form about our custom content.
    $form[$this->formElementName] = array(
      '#type' => 'item',
      '#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;
  }

  /**
   * Overrides CqQuestionAbstract::handleNode()
   */
  public function handleNode(\DOMNode $node, $delay = FALSE) {
    $answer = $this->userAnswer->getAnswer();
    if (mb_strtolower($node->nodeName) == 'inlinechoice') {
      $choice = array();

      $attribs = $node->attributes;
      $item = $attribs->getNamedItem('identifier');
      if ($item === NULL) {
        $item = $attribs->getNamedItem('id');
      }
      if ($item !== NULL) {
        $choiceid = $item->nodeValue;
      }
      else {
        if ($this->currentUser->hasPermission(CLOSEDQUESTION_RIGHT_CREATE)) {
          $this->messenger->addMessage(t('Warning: inlinechoice without identifier.'), 'warning');
        }
        $choiceid = 'noId';
      }

      $choice['#parentCqid'] = $this->closedQuestion->id();
      $choice['#id'] = $choiceid;
      $freeform = $node->attributes->getNamedItem('freeform');
      $longtext = $node->attributes->getNamedItem('longtext');
      $style = $node->attributes->getNamedItem('style');
      $groupItem = $node->attributes->getNamedItem('group');
      if ($groupItem == NULL || empty($groupItem->nodeValue)) {
        $choice['#group'] = 'default';
      }
      else {
        $choice['#group'] = $groupItem->nodeValue;
      }
      if (!isset($this->inlineOptionsByGroup[$choice['#group']])) {
        $choice['#group'] = 'default';
      }

      if ($style != NULL) {
        $choice['#style'] = $style->nodeValue;
      }

      $class = $node->attributes->getNamedItem('class');
      if ($class != NULL) {
        $choice['#class'] = $class->nodeValue;
      }
      $choice['#name'] = '' . $this->formElementName . '[' . $choiceid . ']';

      $choice['#size'] = 8;
      $sizeAtt = $node->attributes->getNamedItem('size');
      if ($sizeAtt != NULL) {
        $choice['#size'] = $sizeAtt->nodeValue;
      }


      $choice['#value'] = isset($answer[$choiceid]) ? Html::escape($answer[$choiceid]) : '';

      $choice['#freeform'] = 0;
      if ($freeform != NULL && $freeform->nodeValue) {
        $choice['#freeform'] = $freeform->nodeValue;
      }

      if (!$choice['#freeform']) {
        $choice['#options'] = $this->inlineOptionsByGroup[$choice['#group']];
      }

      $this->inlineChoices[$choiceid] = $choice;

      if ($longtext != NULL && $longtext->nodeValue) {
        $choice['#longtext'] = $longtext->nodeValue;
      }

      $choice['#theme'] = 'closedquestion_inline_choice';
      $retval = $this->renderer->render($choice)->__toString();
    }
    else {
      $retval = parent::handleNode($node, $delay);
    }

    return $retval;
  }

  /**
   * Implements CqQuestionAbstract::getAllText()
   */
  public function getAllText() {
    $this->initialise();
    $retval = array();

    $retval['text']['#markup'] = $this->text;

    // Inline options.
    $retval['options'] = array(
      '#theme' => 'closedquestion_inline_option_list',
      'items' => array(),
      '#extended' => TRUE,
    );
    foreach ($this->inlineOptions as $option) {
      $retval['options']['items'][] = $option;
    }

    // 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();
      }
    }

    // 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;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc