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

src/Question/CqQuestionSelectOrder.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 CqQuestionSelectOrder.
 *
 * Implementation of the Select & Order question type.
 *
 * A select & order question displays a list of options to the student and
 * one or more target boxes. The student drags options to the target boxes, and
 * orders the selected options.
 *
 * @package Drupal\closedquestion\Question
 */
class CqQuestionSelectOrder 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;

  /**
   * Title of the box containing the un-selected options.
   *
   * @var string
   */
  private $sourceTitle;

  /**
   * Title of the target-box if there is only one target box.
   *
   * @var string
   */
  private $sectionTitle;

  /**
   * Allow duplicates?
   *
   * If 1, a student can select one option more than once. If any other value,
   * a student can use an option only once.
   *
   * @var int
   */
  private $duplicates = 0;

  /**
   * The complete list of items that the user can select.
   *
   * @var \Drupal\closedquestion\Question\CqOption[]
   *   Associative array, keys are the opion identifiers.
   */
  private $items = array();

  /**
   * Feedback mappings.
   *
   * @var \Drupal\closedquestion\Question\Mapping\CqMapping[]
   */
  private $mappings = array();

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

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

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

  /**
   * The state to use when the student has not yet changed any thing.
   *
   * @var string
   */
  private $defaultAnswer = '';

  /**
   * The list of items that are free for the user to select.
   *
   * Either because he has not selected them yet, or because duplicates
   * are allowed.
   *
   * @var \Drupal\closedquestion\Question\CqOption[]
   *   Associative array, keys are the opion identifiers.
   */
  private $unSelected = array();

  /**
   * The list of sections (target boxes).
   *
   * Each section is represented as a CqOption with a numeric identifier.
   *
   * @var \Drupal\closedquestion\Question\CqOption[]
   */
  private $sections = array();

  /**
   * The list of selected items, ordered by section.
   *
   * @var array
   *   Associative array, with section id's as key, containing assocaitive
   *   arrays, with item id's as key, containing CqOptions.
   *   Map<String, Map<String, CqOption>>
   */
  private $selectedBySection = array();

  /**
   * Alignment key.
   *
   * (Default) normal: options are below each other, source box on the left,
   * target boxes on the right
   * horizontal: options are next to each other, source box at the top, target
   * boxes below that.
   *
   * @var string
   */
  private $alignment = 'normal';

  /**
   * Minimal height.
   *
   * Used for items to force nice alignment if contents are of
   * varying height.
   *
   * @var int|bool
   *   FALSE if not set.
   */
  private $optionHeight = FALSE;


  /**
   * Determines whether question is only order (and no select)
   *
   * @var int|bool
   *   FALSE if not set.
   */
  private $onlyOrder = 0;

  /**
   * Constructs a Select&Order 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->sourceTitle = t('Available Items');
    $this->sectionTitle = t('Selected Items');
    $this->userAnswer = $userAnswer;
    $this->closedQuestion = $closedQuestion;
    $this->formElementName = 'cq_so_' . $this->closedQuestion->id() . '_';
  }

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

  /**
   * Implements CqQuestionAbstract::getForm()
   */
  public function getForm($formState) {
    $answer = $this->userAnswer->getAnswer();
    $this->sortItems();

    // The question part is themed.
    $form['question']['#theme'] = 'closedquestion_question_select_order';
    $form['question']['questionText'] = array(
      '#type' => 'item',
      '#markup' => $this->text,
    );

    // The data needed by the theme function.
    $data = array();
    $data['elementname'] = $this->formElementName;
    $data['duplicates'] = $this->duplicates;
    $data['alignment'] = $this->alignment;
    $data['optionHeight'] = $this->optionHeight;
    $data['sourceTitle'] = $this->sourceTitle;
    $data['onlyOrder'] = $this->onlyOrder;

    $selected = '';

    foreach ($this->unSelected as $item) {
      $data['unselected'][] = array(
        '#identifier' => $item->getIdentifier(),
        '#text' => $item->getText(),
        '#description' => $item->getDescription(),
      );
    }

    foreach ($this->selectedBySection as $sectionId => $sectionSelected) {
      if (isset($this->sections[$sectionId])) {
        $sectionItem = $this->sections[$sectionId];
        $sectionData = array(
          '#text' => $sectionItem->getText(),
          '#identifier' => $sectionItem->getIdentifier(),
          'items' => array(),
        );
        $selected .= $sectionItem->getIdentifier();
      }
      else {
        $sectionData = array(
          '#text' => $this->sectionTitle,
          '#identifier' => '',
          'items' => array(),
        );
      }

      foreach ($sectionSelected as $item) {
        $sectionData['items'][] = array(
          '#identifier' => $item->getIdentifier(),
          '#text' => $item->getText(),
          '#description' => $item->getDescription(),
        );
        $selected .= $item->getIdentifier();
      }
      $data['sections'][] = $sectionData;
    }
    $form['question']['data'] = array('#type' => 'value', '#value' => $data);

    // This element will be filled by Javascript to contain the answer.
    $form[$this->formElementName . 'selected'] = array(
      '#type' => 'hidden',
      '#input' => TRUE,
      '#default_value' => $selected,
    );

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

  /**
   * Sorts answer items.
   *
   * Parses the student's answer ans sorts the items according to the selection
   * the student made.
   */
  private function sortItems() {
    $answer = $this->userAnswer->getAnswer();

    $this->unSelected = array();
    foreach ($this->selectedBySection as $id => $list) {
      $this->selectedBySection[$id] = array();
    }

    foreach ($this->items as $key => $item) {
      if (!is_numeric($key) && ($this->duplicates || !$answer || !strstr($answer, $key))) {
        $this->unSelected[] = $item;
      }
    }
    $curSection = 1;
    foreach (str_split($answer) as $key) {
      if (isset($this->sections[$key])) {
        $curSection = $key;
      }
      elseif (isset($this->items[$key])) {
        $this->selectedBySection[$curSection][] = $this->items[$key];
      }
    }
  }

  /**
   * Implements CqQuestionAbstract::getFeedbackItems()
   */
  public function getFeedbackItems() {
    $answer = $this->userAnswer->getAnswer();
    $tries = $this->userAnswer->getTries();
    $feedback = array();

    if ($tries == 0 && $answer == $this->defaultAnswer) {
      // 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.
    $feedback = array_merge($feedback, $this->fireGetExtraFeedbackItems($this, $tries));
    return $feedback;
  }

  /**
   * Implements CqQuestionAbstract::checkCorrect()
   */
  public function checkCorrect() {
    $this->matchedMappings = array();
    $this->matchedCorrectMappings = array();
    $answer = $this->userAnswer->getAnswer();
    $tries = $this->userAnswer->getTries();

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

  /**
   * 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':
        case 'item':
          $option = new CqOption($node, $this);
          if (is_numeric($option->getIdentifier())) {
            $this->sections[$option->getIdentifier()] = $option;
            $this->selectedBySection[$option->getIdentifier()] = array();
          }
          if (isset($this->items[$option->getIdentifier()])) {
            $this->messenger->addMessage(t('Option identifier %identifier used more than once!', array('%identifier' => $option->getIdentifier())), 'warning');
          }
          $this->items[$option->getIdentifier()] = $option;
          break;

        // Sequence: Old style sequence, would stop at first match.
        // Mapping: New style mapping, continues at match by default.
        case 'sequence':
        case 'mapping':
          $map = new CqMapping();
          $map->generateFromNode($node, $this);

          if ($node->nodeName == 'sequence') {
            $map->setStopIfMatch(TRUE);
          }

          $this->mappings[] = $map;
          break;

        case 'text':
          $this->text = $this->xmlLib->getTextContent($node, $this);
          break;

        case 'hint':
          $this->hints[] = CqFeedback::newCqFeedback($node, $this);
          break;

        case 'default':
        case 'startstate':
          $attribs = $node->attributes;
          $item = $attribs->getNamedItem('value');
          if ($item !== NULL) {
            $this->defaultAnswer = $item->value;
          }
          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('duplicates');
    if ($item !== NULL) {
      $this->duplicates = (int) $item->value;
    }
    $item = $attribs->getNamedItem('alignment');
    if ($item !== NULL) {
      $this->alignment = $item->value;
    }
    $item = $attribs->getNamedItem('optionheight');
    if ($item !== NULL) {
      $this->optionHeight = $item->value;
    }
    $item = $attribs->getNamedItem('onlyorder');
    if ($item !== NULL) {
      $this->onlyOrder = $item->value;
    }

    $answer = $this->userAnswer->getAnswer();
    if (empty($answer)) {
      $this->userAnswer->setAnswer($this->defaultAnswer);
    }

    if (count($this->selectedBySection) == 0) {
      // No sections defined, make one.
      $this->selectedBySection[1] = array();
    }
  }

  /**
   * Implements CqQuestionAbstract::submitAnswer()
   */
  public function submitAnswer($form, FormStateInterface $form_state) {
    $newAnswer = $form_state->getValue($this->formElementName . 'selected');
    if ($this->currentUser->hasPermission(CLOSEDQUESTION_RIGHT_CREATE)) {
      $this->messenger->addMessage(t('Answer=%answer', array('%answer' => $newAnswer)));
    }
    $this->userAnswer->setAnswer($newAnswer);
    $correct = $this->isCorrect(TRUE);
    if ($this->userAnswer->answerHasChanged()) {
      if (!$correct) {
        $this->userAnswer->increaseTries();
      }
      $this->userAnswer->store();
    }
  }

  /**
   * 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->userAnswer->getAnswer();
    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.
      return substr($answer, $start, $length);
    }
    else {
      return $answer;
    }
  }

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

    // Options.
    $retval['options'] = array(
      '#theme' => 'closedquestion_option_list',
      'items' => array(),
      '#extended' => TRUE,
    );
    foreach ($this->items 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;
  }

}

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

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