You are here

quiz_question.module in Quiz 8.4

Quiz Question module. This module provides the basic facilities for adding quiz question types to a quiz.

File

question_types/quiz_question/quiz_question.module
View source
<?php

/**
 * Quiz Question module.
 * This module provides the basic facilities for adding quiz question types to a quiz.
 * @file
 */

/*
 * The system remembers what quizzes a user has been involved in lately. This constant determines
 * how many quizzes the system will remember for each user
 */
define('QUIZ_QUESTION_NUM_LATEST', 10);

/**
 * Implements hook_help().
 */
function quiz_question_help($path, $args) {
  if ($path == 'admin/help#quiz_quesion') {
    return t('Support for Quiz question types.');
  }
}

/**
 * Implements hook_menu().
 */
function quiz_question_menu() {
  $items = array();
  $items['quiz_question/%/%/revision_actions'] = array(
    'title' => 'Revision actions',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quiz_question_revision_actions',
      1,
      2,
    ),
    'access arguments' => array(
      'manual quiz revisioning',
    ),
    'file' => 'quiz_question.pages.inc',
    'type' => MENU_NORMAL_ITEM,
  );

  //TODO: Need to verify

  //Empty form

  // Menu items for admin view of each question type.
  $items['admin/config/quiz/questions_settings'] = array(
    'title' => 'Question configuration',
    'description' => 'Configure the question types.',
    'route_name' => 'quiz.question_config',
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Implements hook_theme().
 */
function quiz_question_theme() {
  return array(
    'quiz_question_creation_form' => array(
      'render element' => 'form',
      'file' => 'quiz_question.theme.inc',
    ),
    'quiz_question_navigation_form' => array(
      'render element' => 'form',
      'file' => 'quiz_question.theme.inc',
    ),
  );
}

/**
 * Implements hook_node_access().
 */
function quiz_question_node_access($node, $op, $account) {

  // We could use the "$node->is_quiz_question" property to check if it's a question type node or not.
  // But by hook_node_access() definition $node can be a node object or the machine name of the
  // content type. Using _quiz_question_get_implementations() as a workaround for this.
  $question_types = array_keys(_quiz_question_get_implementations());
  $type = is_string($node) ? $node : $node
    ->getType();
  if (!in_array($type, $question_types)) {
    return NODE_ACCESS_IGNORE;
  }
  switch ($op) {
    case 'view':
      if (!user_access('view quiz question outside of a quiz')) {
        return NODE_ACCESS_DENY;
      }
      break;
    case 'create':
      if (!user_access('create quiz content', $account)) {
        return NODE_ACCESS_DENY;
      }
      break;
    case 'update':
      if (user_access('edit any quiz content', $account) || user_access('edit own quiz content', $account) && $account->uid == $node->uid) {
        return NODE_ACCESS_ALLOW;
      }
      break;
    case 'delete':
      if (user_access('delete any quiz content', $account) || user_access('delete own quiz content', $account) && $account->uid == $node->uid) {
        return NODE_ACCESS_ALLOW;
      }
      break;
  }

  // Returning nothing from this function would have the same effect.
  return NODE_ACCESS_IGNORE;
}

/**
 * Figure out if a user has access to score a certain result
 *
 * @param $vid
 *  Question version id
 * @param $rid
 *  Result id
 * @return
 *  True if the user has access to score the result
 */
function quiz_question_access_to_score($vid, $rid) {
  global $user;
  $sql = 'SELECT * FROM {quiz_node_results_answers} WHERE result_id = :result_id AND question_vid = :question_vid';
  $answer = db_query($sql, array(
    ':result_id' => $rid,
    ':question_vid' => $vid,
  ))
    ->fetch();
  if (!$answer) {
    return FALSE;
  }
  if (user_access('score any quiz')) {
    return TRUE;
  }
  if (user_access('score taken quiz answer')) {
    $uid = db_query('SELECT uid from {quiz_node_results} qnr WHERE qnr.result_id = :result_id', array(
      ':result_id' => $rid,
    ))
      ->fetchField();
    if ($uid == $user->uid) {
      return TRUE;
    }
  }
  if (user_access('score own quiz')) {
    return db_query('SELECT r.uid FROM {node_revision} r
            JOIN {quiz_node_results} qnr ON (r.nid = qnr.nid)
            WHERE qnr.result_id = :result_id
            ', array(
      ':result_id' => $rid,
    ))
      ->fetchField() == $user->uid;
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().  
 * 
 */
function quiz_question_form_node_form_alter(&$form, &$form_state, $form_id) {
  $quiz_question_types = array_keys(_quiz_question_get_implementations());
  $node = $form_state['controller']
    ->getEntity();
  if (in_array($node
    ->getType(), $quiz_question_types)) {
    $question = _quiz_question_get_instance($node);
    if ($question) {
      $form = array_merge($form, $question
        ->getNodeForm($form_state));
    }

    //Rearrange body field
    $form['body']['widget']['#weight'] = 10;
    $form['#validate'] = array(
      'quiz_question_form_validate',
    );
    $form['#entity_builders'][] = 'quiz_question_form_builder';
  }
  return $form;
}

/**
 * Form element validation handler.
 *
 * @see quiz_question_form_alter()
 */
function quiz_question_form_validate($form, &$form_state) {

  //Build node
  $node = $form_state['controller']
    ->getEntity();

  // Check to make sure that there is a question.
  if (empty($form_state['values']['body'][0]['value'])) {
    form_set_error('body', $form_state, t('Question text is empty.'));
  }
  _quiz_question_get_instance($node)
    ->validateNode($form_state);
}

/**
 * Entity form builder to add the quiz_question information to the node.
 */
function quiz_question_form_builder($entity_type, $entity, &$form, &$form_state) {
  _quiz_question_get_instance($entity)
    ->entityBuilder($form_state);
}

/**
 *  Submit handler for quiz config settings form.
 * @todo: find proper way to do it.
 */
function quiz_question_config_submit($form, &$form_state) {
  drupal_set_message(t('The configuration options have been saved.'));
}

/**
 * Get the form to show to the quiz taker.
 */
function quiz_question_answering_form($form, $form_state, $node, $include_nid_in_id = FALSE) {
  $question = _quiz_question_get_instance($node);
  $form = $question
    ->getAnsweringForm($form_state, isset($node->rid) ? $node->rid : NULL);
  $quiz = quiz_type_access_load(arg(1));
  $form['#attributes']['class'] = array(
    'answering-form',
  );
  $is_last = _quiz_is_last_question();
  $form['navigation']['#theme'] = 'quiz_question_navigation_form';
  if ($quiz->mark_doubtful) {
    $form['is_doubtful'] = array(
      '#type' => 'checkbox',
      '#title' => t('doubtful'),
      '#weight' => 1,
      '#prefix' => '<div class="mark-doubtful checkbox enabled"><div class="toggle"><div></div></div>',
      '#suffix' => '</div>',
      '#default_value' => 0,
      '#attached' => array(
        'js' => array(
          drupal_get_path('module', 'quiz') . '/templates/quiz_take.js',
        ),
      ),
    );
    if (isset($node->rid)) {
      $form['is_doubtful']['#default_value'] = db_query('SELECT is_doubtful FROM {quiz_node_results_answers} WHERE result_id = :result_id AND question_nid = :question_nid AND question_vid = :question_vid', array(
        ':result_id' => $node->rid,
        ':question_nid' => $node
          ->id(),
        ':question_vid' => $node
          ->getRevisionId(),
      ))
        ->fetchField();
    }
  }
  if (!empty($quiz->backwards_navigation) && !empty($node->question_number)) {
    $form['navigation']['back'] = array(
      '#type' => 'submit',
      '#value' => t('Back'),
      '#attributes' => array(
        'class' => array(
          'q-back-button',
        ),
      ),
    );
    if ($is_last) {
      $form['navigation']['#last'] = TRUE;
    }
  }

  // Add navigation at the bottom:
  $form['navigation']['submit'] = array(
    '#type' => 'submit',
    '#value' => $is_last ? t('Finish') : t('Next'),
  );
  if ($quiz->allow_skipping) {
    $form['navigation']['op'] = array(
      '#type' => 'submit',
      '#value' => $is_last ? t('Leave blank and finish') : t('Leave blank'),
      '#attributes' => array(
        'class' => array(
          'q-skip-button',
        ),
      ),
    );
    if ($quiz->allow_jumping) {
      $form['jump_to_question'] = array(
        '#type' => 'hidden',
        '#default_value' => 0,
      );
    }
  }
  return $form;
}

/**
 * Form for teaser display
 *
 * @param $node
 *  The question node
 * @return
 *  Content array
 */
function _quiz_question_teaser_content($node) {
  $content['question_type'] = array(
    '#markup' => '<div class="question_type_name">' . node_type_load($node
      ->getType())->name . '</div>',
    '#weight' => -100,
  );
  return $content;
}

/**
 * Implements hook_evaluate_question().
 *
 * @param $question
 *  The question node
 * @param $result_id
 *  Result id
 * @param $answer
 *  The users answer to the question
 * @return
 *  Object with nid, vid, rid, score, is_correct flags set.
 */
function quiz_question_evaluate_question($question, $result_id, $answer = NULL) {
  if (empty($answer) && isset($_POST['tries'])) {

    // FIXME this use of POST array is hacky. We will try to use FAPI mor accurately in Quiz 5.x
    $answer = $_POST['tries'];
  }
  unset($_POST['tries']);
  $response = _quiz_question_response_get_instance($result_id, $question, $answer);

  // If a result_id is set, we are taking a quiz.
  if ($result_id && isset($answer)) {

    // We don't know whether or not the user has gone back a question. However,
    // we do know that deleting a question for this result set should be safe in
    // the case where the user has not gone back (since there will be no entries
    // to delete). So we always run the delete.
    $response
      ->delete();
    $response
      ->saveResult();
  }

  // Convert the response to a bare object.
  return $response
    ->toBareObject();
}

/**
 * Implements hook_skip_question().
 */
function quiz_question_skip_question($question, $result_id) {
  unset($_POST['tries']);

  // Unset any answer that might have been set.
  // Delete any old answers for this question (for backwards nav).
  _quiz_question_response_get_instance($result_id, $question)
    ->delete();

  // This is the standard response:
  $response = new stdClass();
  $response->nid = $question
    ->id();
  $response->vid = $question
    ->getRevisionId();
  $response->rid = $result_id;
  $response->is_skipped = TRUE;
  if (isset($_POST['is_doubtful'])) {
    $response->is_doubtful = $_POST['is_doubtful'];
  }
  else {
    $response->is_doubtful = db_query('SELECT is_doubtful FROM {quiz_node_results_answers} WHERE result_id = :result_id AND question_nid = :question_nid AND question_vid = :question_vid', array(
      ':result_id' => $result_id,
      ':question_nid' => $question
        ->id(),
      ':question_vid' => $question
        ->getRevisionId(),
    ))
      ->fetchField();
  }
  return $response;
}

/**
 * Implements hook_list_questions().
 */
function quiz_question_list_questions($count = 0, $offset = 0) {
  $query = db_select('node', 'n')
    ->fields('n', array(
    'nid',
    'vid',
  ))
    ->innerjoin('node_revision', 'nr')
    ->condition('type', array_keys(_quiz_question_get_implementations()), 'IN')
    ->execute();

  /*
  $sql = "SELECT n.nid, n.vid, r.body, r.format FROM {node} AS n
    INNER JOIN {node_revision} AS r USING(vid)
    WHERE n.type IN (%s) ORDER BY n.type, n.changed";
  $types = array();
  foreach (array_keys(_quiz_question_get_implementations()) as $key) {
    $types[] = "'" . $key . "'";
  }
  $type = implode(',', $types);

  if ($count == 0) {
    // Return all results
    // TODO Please convert this statement to the D7 database API syntax.
    $result = db_query(db_rewrite_sql($sql), $type);
  }
  else {
    // return only $count results
    // TODO Please convert this statement to the D7 database API syntax.
    $result = db_query_range(db_rewrite_sql($sql), $type);
  }
  */

  /**
   * The following code doesn't make any sense, since
   * the body is not fetched in the above SQL
   *
   * From where is this function invoked?
   */
  $questions = array();
  while ($question = $query
    ->fetch()) {
    $question->question = check_markup($question->body, $question->body['und'][0]['format']);
    $questions[] = $question;
  }
  return $questions;
}

/**
 * Imlementation of hook_get_report().
 *
 * @return
 *  Node containing all of the items from the question plus the user's answer.
 */
function quiz_question_get_report($nid, $vid, $rid) {
  $response_instance = _quiz_question_response_get_instance($rid, NULL, NULL, $nid, $vid);
  if (!$response_instance) {
    drupal_set_message(t('Unable to load question with nid %nid and vid %vid', array(
      '%nid' => $nid,
      '%vid' => $vid,
    )), 'error');
    return FALSE;
  }
  $result = $response_instance
    ->getReport();
  $response_instance->question->answers[$result['answer_id']] = $result;
  $response_instance->question->correct = $result['is_correct'];
  return $response_instance->question;
}

/**
 * @todo Please document this function.
 * @see http://drupal.org/node/1354
 */
function quiz_question_has_been_answered($node) {
  $question_instance = _quiz_question_get_instance($node, true);
  return $question_instance
    ->hasBeenAnswered();
}

/**
 * Implements hook_quiz_question_score().
 */
function quiz_question_quiz_question_score($quiz, $question_nid, $question_vid = NULL, $rid = NULL) {
  if (!isset($quiz) && !isset($rid)) {
    return quiz_question_get_max_score($question_nid, $question_vid);
  }

  // We avoid using node_load to increase performance...
  $dummy_node = new \stdClass();
  $dummy_node->nid = $question_nid;
  $dummy_node->vid = $question_vid;
  $question = _quiz_question_get_instance($dummy_node, TRUE);
  if (!$question) {
    return FALSE;
  }
  $score = new stdClass();
  $score->possible = $question
    ->getMaximumScore();
  if ($question->node instanceof \stdClass) {
    $score->question_nid = $question->node->nid;
    $score->question_vid = $question->node->vid;
  }
  else {
    $score->question_nid = $question->node
      ->id();
    $score->question_vid = $question->node
      ->getRevisionId();
  }
  if (isset($rid)) {
    $response = _quiz_question_response_get_instance($rid, $question->node);
    $score->attained = $score->possible > 0 ? $response
      ->getScore() : 0;
    $score->possible = $response
      ->getMaxScore();
    $score->is_evaluated = $response
      ->isEvaluated();
  }
  return $score;
}

/**
 * Implements hook_delete_result().
 *
 * @param $rid
 *  Result id
 * @param $nid
 *  Question node id
 * @param $vid
 *  Question node version id
 */
function quiz_question_delete_result($rid, $nid, $vid) {
  $response = _quiz_question_response_get_instance($rid, NULL, NULL, $nid, $vid);
  if ($response) {
    $response
      ->delete();
  }
  else {
    drupal_set_message(t('Unable to delete result. A constructor could not be found for the question-type'), 'error');
  }
}

/**
 * Get the configuration form for all enabled question types.
 */

/*function quiz_question_config($form, $context) {
  $q_types = _quiz_question_get_implementations();
  $form = array();
  $form['#validate'] = array();
  // Go through all question types and merge their config forms
  foreach ($q_types as $type => $values) {
    $function = $type . '_config';
    if ($admin_form = $function()) {
      $form[$type] = $admin_form;
      $form[$type]['#type'] = 'fieldset';
      $form[$type]['#title'] = $values['name'];
      $form[$type]['#collapsible'] = TRUE;
      $form[$type]['#collapsed'] = TRUE;
      if (isset($admin_form['#validate'])) {
        $form['#validate'] = array_merge($form['#validate'], $admin_form['#validate']);
	unset($form[$type]['#validate']);
      }
    }
  }
  return system_settings_form($form);
}*/

// NODE API

/**
 * Implements hook_node_revision_delete().
 */
function quiz_question_node_revision_delete($node) {
  $q_types = _quiz_question_get_implementations();
  foreach ($q_types as $q_type => $info) {
    if ($node
      ->getType() == $q_type) {
      _quiz_delete_question($node, TRUE);

      // true for only this version
    }
  }
}

/**
 * Implements hook_node_presave().
 */
function quiz_question_node_presave(\Drupal\Core\Entity\EntityInterface $node) {
  $q_types = _quiz_question_get_implementations();
  foreach ($q_types as $q_type => $info) {
    if ($node
      ->getType() == $q_type) {
      if (drupal_strlen($node
        ->getTitle()) == 0 || !user_access('edit question titles')) {
        $body = field_get_items('node', $node, 'body');
        $markup = strip_tags(check_markup($body[0]['value'], $body[0]['format']));
        if (drupal_strlen($markup) > variable_get('quiz_autotitle_length', 50)) {
          $node->title = drupal_substr($markup, 0, variable_get('quiz_autotitle_length', 50) - 3) . '...';
        }
        else {
          $node->title = $markup;
        }
      }
    }
  }
}

/**
 * Implements hook_node_insert().
 */
function quiz_question_node_insert(\Drupal\Core\Entity\EntityInterface $node) {

  // Make sure the latest quizzes table is maintained when a quiz changes
  if ($node
    ->getType() == 'quiz') {
    quiz_question_refresh_latest_quizzes($node
      ->id());
  }
  $q_types = array_keys(_quiz_question_get_implementations());
  if (in_array($node
    ->getType(), $q_types)) {
    _quiz_question_get_instance($node)
      ->save(TRUE);
    if (isset($node->quiz_nid) && $node->quiz_nid > 0) {
      quiz_question_refresh_latest_quizzes($node->quiz_nid);
    }
  }
}

/**
 * Implements hook_node_update().
 */
function quiz_question_node_update(\Drupal\Core\Entity\EntityInterface $node) {

  // Make sure the latest quizzes table is maintained when a quiz changes
  if ($node
    ->getType() == 'quiz') {
    quiz_question_refresh_latest_quizzes($node
      ->id());
  }
  $q_types = array_keys(_quiz_question_get_implementations());
  if (in_array($node
    ->getType(), $q_types)) {
    _quiz_question_get_instance($node)
      ->save();
  }
}

/**
 * Implements hook_node_delete().
 */
function quiz_question_node_delete($node) {

  // Make sure the latest quizzes table is maintained when a quiz changes
  if ($node
    ->getType() == 'quiz') {
    quiz_question_remove_latest_quizzes($node
      ->id());
  }
  $q_types = array_keys(_quiz_question_get_implementations());
  if (in_array($node
    ->getType(), $q_types)) {
    _quiz_delete_question($node, FALSE);
  }
}

/**
 * Implements hook_node_view().
 */
function quiz_question_node_view(\Drupal\Core\Entity\EntityInterface $node, \Drupal\entity\Entity\EntityDisplay $display, $view_mode, $langcode) {
  $q_types = array_keys(_quiz_question_get_implementations());
  if (in_array($node
    ->getType(), $q_types)) {
    drupal_add_css(drupal_get_path('module', 'quiz') . '/quiz.css');
    if ($view_mode == 'search_index' && !\Drupal::config('quiz_question.settings')
      ->get('quiz_index_questions')) {
      $node->body = '';
      $node->content = array();
      $node->title = '';
      $node->taxonomy = array();

      //return $node;
    }
    $content = '';
    if (_quiz_is_taking_context()) {

      /*
       * @todo: I see no reason why this should be a part of view anymore.
       * In quiz 5 we should stop using hook_view to view the answering form
       */
      $form = drupal_get_form('quiz_question_answering_form', $node);
      $form_markup = drupal_render($form);
      if (!empty($form_markup)) {
        $node->content['body'] = array(
          '#markup' => $form_markup,
        );
      }
    }
    elseif ($view_mode == 'teaser') {
      $node->content['question_teaser'] = _quiz_question_teaser_content($node);
    }
    else {

      // normal node view

      //$question = _quiz_question_get_instance($node, TRUE);
      $content = _quiz_question_get_instance($node, TRUE)
        ->getNodeView();
    }

    // put it into the node->content
    if (!empty($content)) {
      $node->content = isset($node->content) ? $node->content + $content : $content;
    }
  }
}

/**
 * Delete the question node from the db, and mark its identifiers in the quiz
 * linking table as "NEVER".  This is safer than deleting them
 * and allows for same tracing of what's happened if a question was deleted unintentionally.
 *
 * @param $node the question node
 * @param $only_this_version whether to delete only the specific revision of the question
 */
function _quiz_delete_question(&$node, $only_this_version) {

  // let each question class delete its own stuff
  _quiz_question_get_instance($node, TRUE)
    ->delete($only_this_version);

  // FIXME QuizQuestion class makes these relationships, so it should handle their 'deletion' too
  // FIXME alternately, move the relationship handling out of QuizQuestion class
  // @todo reconsider this QUESTION_NEVER status, since the node is actually gone
  // then remove it from {quiz_node_relationship} linking table

  //$base_sql = "UPDATE {quiz_node_relationship} SET question_status = " . QUESTION_NEVER;
  $select_sql = 'SELECT parent_vid FROM {quiz_node_relationship}';
  if ($only_this_version) {
    $select_sql .= ' WHERE child_nid = :child_nid AND child_vid = :child_vid';
    $filter_arg = array(
      ':child_nid' => $node
        ->id(),
      ':child_vid' => $node
        ->getRevisionId(),
    );
  }
  else {
    $select_sql .= ' WHERE child_nid = :child_nid';
    $filter_arg = array(
      ':child_nid' => $node
        ->id(),
    );
  }

  //$res = db_query($select_sql . $filter_sql, $node->id(), $node->getRevisionId());
  $res = db_query($select_sql, $filter_arg);

  //db_query($base_sql . $filter_sql, $node->id(), $node->getRevisionId());
  $update = db_update('quiz_node_relationship')
    ->fields(array(
    'question_status' => QUESTION_NEVER,
  ))
    ->condition('child_nid', $node
    ->id());
  if ($only_this_version) {
    $update = $update
      ->condition('child_vid', $node
      ->getRevisionId());
  }
  $update
    ->execute();
  $quizzes_to_update = array();
  while ($quizzes_to_update[] = $res
    ->fetchField()) {
  }
  quiz_update_max_score_properties($quizzes_to_update);
}

/**
 * Implements hook_node_load().
 */
function quiz_question_node_load($nodes) {
  $q_types = array_keys(_quiz_question_get_implementations());
  foreach ($nodes as $nid => $node) {
    if (in_array($node
      ->getType(), $q_types)) {
      $node_additions = _quiz_question_get_instance($node, TRUE)
        ->getNodeProperties();
      foreach ($node_additions as $property => $value) {
        $node->{$property} = $value;
      }
    }
  }
}

// END NODE API

/**
 * Get an instance of a quiz question.
 *
 * Get information about the class and use it to construct a new
 * object of the appropriate type.
 *
 * @param $node
 *  Question node
 * @param $use_cached
 *  Can we use a cached version of the node?
 * @return
 *  The appropriate QuizQuestion extension instance
 */
function _quiz_question_get_instance($node, $use_cached = FALSE) {

  // We use static caching to improve performance
  static $question_instances = array();
  $using_dummy_node = FALSE;
  if (is_object($node) && !$node instanceof \stdClass) {
    $vid = $node
      ->getRevisionId() ? $node
      ->getRevisionId() : 0;
    if ($use_cached && isset($question_instances[$vid])) {

      // We just return a cached instance of the QuizQuestion
      return $question_instances[$vid];
    }
    $name = $node
      ->getType();
  }
  elseif (is_object($node) && $node instanceof \stdClass) {
    $vid = $node->vid ? $node->vid : 0;
    if ($use_cached && isset($question_instances[$vid])) {

      // We just return a cached instance of the QuizQuestion
      return $question_instances[$vid];
    }

    // If $node don't have a type it is a dummy node
    if (!isset($node->type) || !($name = $node->type)) {

      // To substanitally improve performance(especially on the result page) we avoid node_load()...
      $sql = 'SELECT d.type, r.nid, r.vid, r.title, p.max_score
              FROM {node_field_revision} r
              JOIN {node} n ON r.nid = n.nid
              JOIN {node_field_data} d ON d.nid = n.nid
              JOIN {quiz_question_properties} p ON r.vid = p.vid
              WHERE r.vid = :vid';
      $node = db_query($sql, array(
        ':vid' => $node->vid,
      ))
        ->fetch();

      // @todo: Do it in proper way.
      $node = (array) $node;
      $node = entity_create('node', $node);
      $name = $node
        ->getType();
      $using_dummy_node = TRUE;
    }
  }
  elseif (is_array($node)) {
    $name = $node['type'];
    $vid = $node['vid'];
    if ($use_cached && isset($question_instances[$vid])) {

      // We return a cached instance of the appropriate QuizQuestion
      return $question_instances[$vid];
    }
  }

  // No cached instance of QuizQuestion has been returned. We construct a new instance
  $info = _quiz_question_get_implementations();
  $constructor = $info[$name]['question provider'];
  if (empty($constructor)) {
    return FALSE;
  }

  // We create a new instance of QuizQuestion
  $to_return = new $constructor($node);
  if (!$to_return instanceof Drupal\quiz_question\QuizQuestion) {

    // Make sure the constructor is creating an extension of QuizQuestion
    drupal_set_message(t('The question-type %name isn\'t a QuizQuestion. It needs to extend the QuizQuestion class.', array(
      '%name' => $name,
    )), 'error', FALSE);
  }

  // If we're using a dummy node we have to run getNodeProperties, and populate the node with those properties
  if ($using_dummy_node) {
    $props = $to_return
      ->getNodeProperties();
    foreach ($props as $key => $value) {
      $to_return->node->{$key} = $value;
    }
  }

  // Cache the node
  $question_instances[$vid] = $to_return;
  return $to_return;
}

/**
 * Get an instance of a quiz question responce.
 *
 * Get information about the class and use it to construct a new
 * object of the appropriate type.
 *
 * @param $rid
 *  Result id
 * @param $question
 *  The question node(not a QuizQuestion instance)
 * @param $answer
 *  Resonce to the answering form.
 * @param $nid
 *  Question node id
 * @param $vid
 *  Question node version id
 * @return
 *  The appropriate QuizQuestionResponce extension instance
 */
function _quiz_question_response_get_instance($rid, $question, $answer = NULL, $nid = NULL, $vid = NULL) {

  // We cache responses to improve performance
  static $quiz_responses = array();
  if (is_object($question)) {
    $revision_id = $question instanceof \stdClass ? $question->vid : $question
      ->getRevisionId();
    $type = $question instanceof \stdClass ? $question->type : $question
      ->getType();
    if (isset($quiz_responses[$rid][$revision_id])) {

      // We refresh the question node in case it has been changed since we cached the response
      $quiz_responses[$rid][$revision_id]
        ->refreshQuestionNode($question);
      if ($quiz_responses[$rid][$revision_id]->is_skipped !== FALSE) {
        return $quiz_responses[$rid][$revision_id];
      }
    }
  }
  elseif (isset($quiz_responses[$rid][$vid])) {
    if ($quiz_responses[$rid][$vid]->is_skipped !== FALSE) {
      return $quiz_responses[$rid][$vid];
    }
  }
  if (!isset($quiz_responses[$rid])) {

    // Prepare to cache responses for this result id
    $quiz_responses[$rid] = array();
  }

  // If the question node isn't set we fetch it from the QuizQuestion instance this responce belongs to
  if (!isset($question)) {
    $dummy_node = new stdClass();
    $dummy_node->nid = $nid;
    $dummy_node->vid = $vid;
    $question = _quiz_question_get_instance($dummy_node, TRUE)->node;

    // Quick fix. convert stdClass object to Node object.
    if ($question instanceof \stdClass) {
      $dummy_node = array();
      $dummy_node = (array) $question;
      $question = entity_create('node', $dummy_node);
    }
  }
  if (!$question) {
    return FALSE;
  }
  $info = _quiz_question_get_implementations();
  $revision_id = $question instanceof \stdClass ? $question->vid : $question
    ->getRevisionId();
  $type = $question instanceof \stdClass ? $question->type : $question
    ->getType();
  $constructor = $info[$type]['response provider'];
  $to_return = new $constructor($rid, $question, $answer);

  // All responce classes must extend QuizQuestionResponse
  if (!$to_return instanceof Drupal\quiz_question\QuizQuestionResponse) {
    drupal_set_message(t('The question-response isn\'t a QuizQuestionResponse. It needs to extend the QuizQuestionResponse interface, or extend the abstractQuizQuestionResponse class.'), 'error', FALSE);
  }

  // Cache the responce instance
  $quiz_responses[$rid][$revision_id] = $to_return;
  return $to_return;
}

/**
 * Get the information about various implementations of quiz questions.
 *
 * @param $reset
 *  If this is true, the cache will be reset.
 * @return
 *  An array of information about quiz question implementations.
 * @see quiz_question_quiz_question_info() for an example of a quiz question info hook.
 */
function _quiz_question_get_implementations($name = NULL, $reset = FALSE) {
  static $info = array();
  if (empty($info) || $reset) {
    $qtypes = \Drupal::moduleHandler()
      ->invokeAll('quiz_question_info');
    foreach ($qtypes as $type => $definition) {

      // We only want the ones with classes.
      if (!empty($definition['question provider'])) {

        // Cache the info
        $info[$type] = $definition;
      }
    }
  }
  return $info;
}

/**
 * Refreshes the quiz_question_latest_quizzes table when a user has modified a new quiz.
 *
 * The latest quizzes table is used to know what quizzes the user has been using lately.
 *
 * @param $nid
 *   nid of the last quiz the current user modified
 */
function quiz_question_refresh_latest_quizzes($nid) {
  $user = \Drupal::currentUser();

  // Delete entry if it allready exists
  db_delete('quiz_question_latest_quizzes')
    ->condition('uid', $user
    ->id())
    ->condition('quiz_nid', $nid)
    ->execute();

  // Inserts as new entry to get new id. Latest quizzes are ordered by id(descending)
  $id = db_insert('quiz_question_latest_quizzes')
    ->fields(array(
    'uid' => $user
      ->id(),
    'quiz_nid' => $nid,
  ))
    ->execute();

  // If we have to many entries for current user, delete the oldest entries...
  $min_id = db_select('quiz_question_latest_quizzes', 'lq')
    ->fields('lq', array(
    'id',
  ))
    ->condition('uid', $user
    ->id())
    ->orderBy('id', 'DESC')
    ->range(QUIZ_QUESTION_NUM_LATEST - 1, 1)
    ->execute()
    ->fetchField();

  // Delete all table entries older than the nth row, if nth row was found.
  if ($min_id) {
    db_delete('quiz_question_latest_quizzes')
      ->condition('id', $min_id, '<')
      ->condition('uid', $user
      ->id())
      ->execute();
  }
}

/**
 * Removes a quiz from the quiz_question_latest_quizzes table.
 *
 * @param $nid
 *   the nid of a quiz that shall be removed
 */
function quiz_question_remove_latest_quizzes($nid) {
  db_delete('quiz_question_latest_quizzes')
    ->condition('quiz_nid', $nid)
    ->execute();
}

/**
 * Get the max score for a question
 *
 * @param $nid
 *  Question node id
 * @param $vid
 *  Question node version id
 * @return
 *  Max score(int)
 */
function quiz_question_get_max_score($nid, $vid) {
  return db_query('SELECT max_score
          FROM {quiz_question_properties}
          WHERE nid = :nid AND vid = :vid', array(
    ':nid' => $nid,
    ':vid' => $vid,
  ))
    ->fetchField();
}

/**
 * Implements hook_field_extra_fields().
 */
function quiz_question_field_extra_fields() {
  $extra = array();
  $question_types = array_keys(_quiz_question_get_implementations());
  if (!empty($question_types)) {
    foreach ($question_types as $type_name) {
      $extra['node'][$type_name]['form']['add_directly'] = array(
        'label' => t('Add directly'),
        'description' => t('Fieldset for adding a question directly into quizzes'),
        'weight' => -3,
      );
    }
  }
  return $extra;
}

/**
 * Returns a result report for a question response.
 *
 * The retaurned value is a form array because in some contexts the scores in the form
 * is editable
 *
 * @param $question
 *  The question node
 * @param $showpoints
 * @param $showfeedback
 * @param $allow_scoring
 * @return
 *  FAPI form array
 */
function quiz_question_report_form($question, $showpoints, $showfeedback, $allow_scoring = FALSE) {
  $answer = $question->answers[0];
  $response_instance = _quiz_question_response_get_instance($answer['result_id'], $question, $answer);

  // If need to specify the score weight if it isn't already specified.
  if (!isset($response_instance->question->score_weight)) {
    $vid = db_query('SELECT vid FROM {quiz_node_results}
      WHERE result_id = :rid', array(
      ':rid' => $answer['result_id'],
    ))
      ->fetchField();
    $qnr_max_score = db_query('SELECT qnr.max_score FROM {quiz_node_relationship} qnr
      WHERE qnr.child_vid = :child_vid AND qnr.parent_vid = :parent_vid', array(
      ':child_vid' => $question
        ->getRevisionId(),
      ':parent_vid' => $vid,
    ))
      ->fetchField();
    if ($qnr_max_score === FALSE) {
      $qnr_max_score = db_query('SELECT qt.max_score FROM {quiz_node_results} qnr
         JOIN {quiz_node_results_answers} qnra ON (qnr.result_id = qnra.result_id)
         JOIN {quiz_terms} qt ON (qt.vid = qnr.vid AND qt.tid = qnra.tid)
         WHERE qnr.result_id = :rid AND qnra.question_nid = :qnid AND qnra.question_vid = :qvid', array(
        ':rid' => $answer['result_id'],
        ':qnid' => $question
          ->id(),
        ':qvid' => $question
          ->getRevisionId(),
      ))
        ->fetchField();
    }
    $response_instance->question->score_weight = $qnr_max_score == 0 || $response_instance->question->max_score == 0 ? 0 : $qnr_max_score / $response_instance->question->max_score;
  }
  return $response_instance
    ->getReportForm($showpoints, $showfeedback, $allow_scoring);
}

Functions

Namesort descending Description
quiz_question_access_to_score Figure out if a user has access to score a certain result
quiz_question_answering_form Get the form to show to the quiz taker.
quiz_question_config_submit Submit handler for quiz config settings form. @todo: find proper way to do it.
quiz_question_delete_result Implements hook_delete_result().
quiz_question_evaluate_question Implements hook_evaluate_question().
quiz_question_field_extra_fields Implements hook_field_extra_fields().
quiz_question_form_builder Entity form builder to add the quiz_question information to the node.
quiz_question_form_node_form_alter Implements hook_form_BASE_FORM_ID_alter().
quiz_question_form_validate Form element validation handler.
quiz_question_get_max_score Get the max score for a question
quiz_question_get_report Imlementation of hook_get_report().
quiz_question_has_been_answered @todo Please document this function.
quiz_question_help Implements hook_help().
quiz_question_list_questions Implements hook_list_questions().
quiz_question_menu Implements hook_menu().
quiz_question_node_access Implements hook_node_access().
quiz_question_node_delete Implements hook_node_delete().
quiz_question_node_insert Implements hook_node_insert().
quiz_question_node_load Implements hook_node_load().
quiz_question_node_presave Implements hook_node_presave().
quiz_question_node_revision_delete Implements hook_node_revision_delete().
quiz_question_node_update Implements hook_node_update().
quiz_question_node_view Implements hook_node_view().
quiz_question_quiz_question_score Implements hook_quiz_question_score().
quiz_question_refresh_latest_quizzes Refreshes the quiz_question_latest_quizzes table when a user has modified a new quiz.
quiz_question_remove_latest_quizzes Removes a quiz from the quiz_question_latest_quizzes table.
quiz_question_report_form Returns a result report for a question response.
quiz_question_skip_question Implements hook_skip_question().
quiz_question_theme Implements hook_theme().
_quiz_delete_question Delete the question node from the db, and mark its identifiers in the quiz linking table as "NEVER". This is safer than deleting them and allows for same tracing of what's happened if a question was deleted unintentionally.
_quiz_question_get_implementations Get the information about various implementations of quiz questions.
_quiz_question_get_instance Get an instance of a quiz question.
_quiz_question_response_get_instance Get an instance of a quiz question responce.
_quiz_question_teaser_content Form for teaser display

Constants