You are here

quiz.module in Quiz 8.4

Quiz Module

This module allows the creation of interactive quizzes for site visitors.

File

quiz.module
View source
<?php

/**
 * @file
 * Quiz Module
 *
 * This module allows the creation of interactive quizzes for site visitors.
 */
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\node\NodeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\entity\Entity\EntityDisplay;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

// This module is structured as follows:
//
// The main module file:
// * Defines and general includes are at the top.
// * Hook implementations come immediately after.
// * Public functions come next.
// * Private functions are at the bottom.
//
// Where possible, user pages are located in quiz.pages.inc, and admin pages
// are in quiz.admin.inc. Most utility functions have been left here, even if they
// are only used by a function in one of the other files. quiz_datetime.inc holds
// some additional date/time functions.
//
// Themes are in quiz.pages.inc unless they clearly only apply to admin screens.
// Then they are in quiz.admin.inc.
//
// Views support is included in includes/views/quiz.views.inc
define('QUIZ_VIEWS_DIR', drupal_get_path('module', 'quiz') . '/includes/views');
module_load_include('inc', 'quiz', 'quiz_datetime');

/*
 * Define question statuses...
 */
define('QUESTION_RANDOM', 0);
define('QUESTION_ALWAYS', 1);
define('QUESTION_NEVER', 2);

/**
 * Quiz name.
 */
define('QUIZ_NAME', _quiz_get_quiz_name());

/**
 * Define feedback statuses.
 */
define('QUIZ_FEEDBACK_END', 0);
define('QUIZ_FEEDBACK_QUESTION', 1);
define('QUIZ_FEEDBACK_NEVER', 2);

/**
 * Define options for keeping results.
 */
define('QUIZ_KEEP_BEST', 0);
define('QUIZ_KEEP_LATEST', 1);
define('QUIZ_KEEP_ALL', 2);

/**
 * Implements hook_help().
 */
function quiz_help($path, $arg) {

  // This is moved on an experimental basis.
  module_load_include('inc', 'quiz', 'quiz.help');
  return _quiz_help($path, $arg);
}

/**
 * Implements hook_views_api().
 *
 * TODO: Convert views api to D8 way.
 */
function quiz_views_api() {
  return array(
    'api' => 2,
    'path' => QUIZ_VIEWS_DIR,
  );
}

/**
 * Implements hook_permission().
 */
function quiz_permission() {
  return array(
    // Configure quiz:
    'administer quiz configuration' => array(
      'title' => t('Administer quiz configuration'),
      'description' => t('Control the various settings and behaviours of quiz'),
      'restrict access' => TRUE,
    ),
    // Managing quizzes:
    'access quiz' => array(
      'title' => t('Take quiz'),
      'description' => t('Can access (take) all quizzes.'),
    ),
    // viewing results:
    'view any quiz results' => array(
      'title' => t('View any quiz results'),
      'description' => t('Can view results for all quizzes and users.'),
    ),
    'view own quiz results' => array(
      'title' => t('View own quiz results'),
      'description' => t('Quiz takers can view their own results, also when quiz is not passed.'),
    ),
    'view results for own quiz' => array(
      'title' => t('View results for own quiz'),
      'description' => t('Quiz makers can view results for their own quizzes.'),
    ),
    // deleting results:
    'delete any quiz results' => array(
      'title' => t('Delete any quiz results'),
    ),
    'delete results for own quiz' => array(
      'title' => t('Delete results for own quiz'),
    ),
    // scoring:
    'score any quiz' => array(
      'title' => t('Score any quiz'),
    ),
    'score own quiz' => array(
      'title' => t('Score own quiz'),
    ),
    'score taken quiz answer' => array(
      'title' => t('score taken quiz answer'),
      'description' => t('Allows attendee to score questions needing manual evaluation.'),
    ),
    // Allow a quiz question to be viewed outside of a test.
    'view quiz question outside of a quiz' => array(
      'title' => t('View quiz question outside of a quiz'),
      'description' => t('Questions can only be accessed through taking a quiz (not as individual nodes) unless this permission is given.'),
    ),
    // Allow the user to see the correct answer, when viewed outside a quiz
    'view any quiz question correct response' => array(
      'title' => t('View any quiz question correct response'),
      'description' => t('Allow the user to see the correct answer, when viewed outside a quiz.'),
    ),
    // Allows users to pick a name for their questions. Otherwise this is auto
    // generated.
    'edit question titles' => array(
      'title' => t('Edit question titles'),
      'description' => t('Questions automatically get a title based on the question text. This allows titles to be set manually.'),
    ),
    // Allow users to assign an action to be performed when a user has completed
    // a quiz:
    'assign any action to quiz events' => array(
      'title' => t('Assign any action to quiz events'),
      'description' => t("Enables Drupal's actions system for Quiz triggers."),
    ),
    // Control revisioning, only assign this permission to users who understand
    // who permissions work. Note: If a quiz or question is changed and not
    // revisioned you will also change existing result reports.
    'manual quiz revisioning' => array(
      'title' => t('Manual quiz revisioning'),
      'description' => t('Quizzes are revisioned automatically each time they are changed. This allows you to do revisions manually.'),
    ),
  );
}

/**
 * Implements hook_admin_paths().
 */
function quiz_admin_paths() {
  if (\Drupal::config('quiz.settings')
    ->get('node_admin_theme')) {
    return array(
      'node/*/questions' => TRUE,
    );
  }
}

/**
 * Helper function to determine if a user has access to the different results
 * pages.
 *
 * @param $quiz
 *   The quiz node.
 * @param $rid
 *   The result id of a result we are trying to access.
 * @return boolean
 *   TRUE if user has permission.
 */
function quiz_access_results($quiz, $rid = NULL) {
  global $user;
  $res = array();
  if ($quiz->type !== 'quiz') {
    return FALSE;
  }

  // If rid is set we must make sure the result belongs to the quiz we are
  // viewing results for.
  if (isset($rid)) {
    $res = db_query('SELECT qnr.nid, qnr.uid FROM {quiz_node_results} qnr WHERE result_id = :result_id', array(
      ':result_id' => $rid,
    ))
      ->fetch();
    if ($res && $res->nid != $quiz
      ->id()) {
      return FALSE;
    }
  }
  if (\Drupal::currentUser()
    ->hasPermission('view any quiz results')) {
    return TRUE;
  }
  if (\Drupal::currentUser()
    ->hasPermission('view results for own quiz') && $user->uid == $quiz->uid) {
    return TRUE;
  }
  if (\Drupal::currentUser()
    ->hasPermission('score taken quiz answer')) {

    //check if the taken user is seeing his result
    if (isset($rid) && $res && $res->uid == $user->uid) {
      return TRUE;
    }
  }
}

/**
 * Helper function to determine if a user has access to view his quiz results
 *
 * @param object $quiz
 *  The Quiz node
 */
function quiz_access_my_results($quiz) {
  global $user;
  if ($quiz->type !== 'quiz') {
    return FALSE;
  }
  if (\Drupal::currentUser()
    ->hasPermission('view own quiz results') && !quiz_access_results($quiz)) {
    $answered = db_query('SELECT 1 FROM {quiz_node_results} WHERE nid = :nid AND uid = :uid AND is_evaluated = :is_evaluated', array(
      ':nid' => $quiz
        ->id(),
      ':uid' => $user->uid,
      ':is_evaluated' => 1,
    ))
      ->fetchField();
    if ($answered) {
      return TRUE;
    }
  }
}

/**
 * Helper function to determine if a user has access to view a specific quiz result.
 *
 * @param int $rid
 *  Result id
 * @return boolean
 *  True if access, false otherwise
 */
function quiz_access_my_result($rid) {
  $user = \Drupal::currentUser();
  if (!\Drupal::currentUser()
    ->hasPermission('view own quiz results')) {
    return FALSE;
  }
  $time_end = db_query('SELECT time_end FROM {quiz_node_results} WHERE result_id = :result_id AND uid = :uid', array(
    ':result_id' => $rid,
    ':uid' => $user
      ->id(),
  ))
    ->fetchField();
  return $time_end > 0;
}

/**
 * Helper function to determine if a user has access to score a quiz.
 *
 * @param $quiz_creator
 *   uid of the quiz creator.
 */
function quiz_access_to_score($quiz_creator = NULL) {
  global $user;
  if ($quiz_creator == NULL && ($quiz = quiz_get_quiz_from_menu())) {
    $quiz_creator = $quiz->uid;
  }
  if (\Drupal::currentUser()
    ->hasPermission('score any quiz')) {
    return TRUE;
  }
  if (\Drupal::currentUser()
    ->hasPermission('score own quiz') && $user->uid == $quiz_creator) {
    return TRUE;
  }
  if (\Drupal::currentUser()
    ->hasPermission('score taken quiz answer')) {
    return TRUE;
  }
}

/**
 * Helper function to check if the user has any of a given list of permissions.
 *
 * @param args
 *   Any number of permissions.
 * @return
 *   TRUE if the user has access to any of the arguments given.
 */
function quiz_access_multi_or() {
  $perms = func_get_args();
  foreach ($perms as $perm) {
    if (\Drupal::currentUser()
      ->hasPermission($perm)) {
      return TRUE;
    }
  }
}

/*
 * Implementation of hook_cron().
 */

/**
 * @todo Please document this function.
 * @see http://drupal.org/node/1354
 */
function quiz_cron() {

  // Remove old quiz results that haven't been finished.
  $rm_time = \Drupal::config('quiz.settings')
    ->get('quiz_remove_partial_quiz_record');
  if ($rm_time) {

    // $time = 0 for never.
    db_delete('quiz_node_results')
      ->condition('time_end', 0)
      ->where('(:request_time - time_start) > :remove_time', array(
      ':request_time' => REQUEST_TIME,
      ':remove_time' => $rm_time,
    ))
      ->execute();
  }
}

/**
 * Implements hook_user_cancel().
 */
function quiz_user_cancel($edit, $account, $method) {
  db_delete('quiz_user_settings')
    ->condition('uid', $account
    ->id())
    ->execute();
  if (variable_get('quiz_durod', 0)) {
    _quiz_delete_users_results($account
      ->id());
  }
}

/**
 * Deletes all results associated with a given user.
 *
 * @param int $uid
 *  The users id
 */
function _quiz_delete_users_results($uid) {
  $res = db_query("SELECT result_id FROM {quiz_node_results} WHERE uid = :uid", array(
    ':uid' => $uid,
  ));
  $rids = array();
  while ($rid = $res
    ->fetchField()) {
    $rids[] = $rid;
  }
  quiz_delete_results($rids);
}

/**
 *  Implements hook_menu().
 *  TODO: Migrate all menu url from D7 Quiz. Total 17 urls.
 */
function quiz_menu() {

  // New configuration block to Quiz.
  $items['admin/config/quiz'] = array(
    'title' => 'Quiz',
    'description' => 'Quiz page',
    'weight' => -10,
    'route_name' => 'quiz.admin_config_quiz',
    'position' => 'left',
  );

  // Quiz Settings.
  $items['admin/config/quiz/config'] = array(
    'title' => '@quiz configuration',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'Configure the Quiz module.',
    'route_name' => 'quiz.settings_config',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/quiz/quiz_form'] = array(
    'title' => '@quiz form configuration',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'Configure default values for the quiz creation form.',
    'route_name' => 'quiz.settings_quiz_form',
    'type' => MENU_NORMAL_ITEM,
  );

  //Quiz Reports.
  $items['admin/config/quiz/results'] = array(
    'title' => '@quiz results',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'View results.',
    'route_name' => 'quiz.settings_results',
    'type' => MENU_NORMAL_ITEM,
  );

  // Converted to D8
  $items['node/%node/options'] = array(
    'title' => 'Options',
    'route_name' => 'quiz.options',
    'type' => MENU_LOCAL_TASK,
    //'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'weight' => 2,
  );
  $items['node/%node/questions'] = array(
    'title' => 'Questions',
    'route_name' => 'quiz.questions',
    'type' => MENU_LOCAL_TASK,
    //'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'weight' => 3,
  );

  // Take quiz. // Converted to D8
  $items['node/%node/take'] = array(
    'title' => 'Take',
    'route_name' => 'quiz.take',
    'type' => MENU_LOCAL_TASK,
    //'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'weight' => 1,
  );

  // Quiz Result.
  $items['node/%node/results'] = array(
    'title' => 'Results',
    'route_name' => 'quiz.results',
    'type' => MENU_LOCAL_TASK,
    'weight' => 3,
  );
  $items['node/%node/results/%quiz_rid'] = array(
    'title' => 'Results',
    'route_name' => 'quiz.results_page',
    'type' => MENU_CALLBACK,
  );
  $items['node/%node/myresults'] = array(
    'title' => 'My results',
    'route_name' => 'quiz.my_results',
    'type' => MENU_LOCAL_TASK,
    'weight' => 3,
  );
  $items['node/%node/myresults/%quiz_rid'] = array(
    'title' => 'User results',
    'route_name' => 'quiz.my_results_page',
    'type' => MENU_CALLBACK,
  );

  // User pages.
  $items['user/%/myresults'] = array(
    'title' => 'My results',
    'route_name' => 'quiz.user_my_results',
    'type' => MENU_LOCAL_TASK,
  );

  //User result
  $items['user/quiz/%/userresults'] = array(
    'title' => 'User results',
    'route_name' => 'quiz.user_results',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_theme().
 */
function quiz_theme() {
  return array(
    'quiz_view_stats' => array(
      'variables' => array(
        'node' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_categorized_form' => array(
      'render element' => 'form',
      'file' => 'quiz.admin.inc',
    ),
    'quiz_get_user_results' => array(
      'variables' => array(
        'results' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_take_summary' => array(
      'variables' => array(
        'quiz' => NULL,
        'questions' => NULL,
        'score' => 0,
        'summary' => '',
        'rid' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_admin_summary' => array(
      'variables' => array(
        'quiz' => NULL,
        'questions' => NULL,
        'score' => NULL,
        'summary' => NULL,
        'rid' => NULL,
      ),
      'file' => 'quiz.admin.inc',
    ),
    'quiz_user_summary' => array(
      'variables' => array(
        'quiz' => NULL,
        'questions' => NULL,
        'score' => NULL,
        'summary' => NULL,
        'rid' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_progress' => array(
      'variables' => array(
        'question_number' => NULL,
        'num_questions' => NULL,
        'allow_jumping' => NULL,
        'time_limit' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_no_feedback' => array(
      'file' => 'quiz.pages.inc',
      'variables' => array(),
    ),
    'quiz_admin_quizzes' => array(
      'file' => 'quiz.admin.inc',
      'variables' => array(
        'results' => NULL,
      ),
    ),
    'quiz_single_question_node' => array(
      'file' => 'quiz.pages.inc',
      'variables' => array(
        'question_node' => NULL,
      ),
    ),
    'question_selection_table' => array(
      'file' => 'quiz.admin.inc',
      'render element' => 'form',
    ),
    'quiz_score_correct' => array(
      'file' => 'quiz.pages.inc',
      'variables' => array(),
    ),
    'quiz_score_incorrect' => array(
      'file' => 'quiz.pages.inc',
      'variables' => array(),
    ),
    'quiz_question_browser' => array(
      'render element' => 'form',
      'template' => 'quiz-question-browser',
    ),
    'quiz_results_browser_body' => array(
      'render element' => 'form',
      'file' => 'quiz.theme.inc',
    ),
    'quiz_results_browser_header' => array(
      'render element' => 'form',
      'file' => 'quiz.theme.inc',
    ),
    'quiz_questions_browser_body' => array(
      'render element' => 'form',
      'file' => 'quiz.theme.inc',
    ),
    'quiz_questions_browser_header' => array(
      'render element' => 'form',
      'file' => 'quiz.theme.inc',
    ),
    'quiz_report_form' => array(
      'render element' => 'form',
      'file' => 'quiz.theme.inc',
    ),
    'quiz_node_form' => array(
      'render element' => 'form',
      'file' => 'quiz.admin.inc',
    ),
    'quiz_browser' => array(
      'render element' => 'form',
      'file' => 'quiz.admin.inc',
    ),
    'quiz_jumper' => array(
      'variables' => array(
        'current' => 0,
        'num_questions' => 0,
      ),
      'file' => 'quiz.admin.inc',
    ),
    'quiz_my_results_for_quiz' => array(
      'variables' => array(
        'rows' => array(),
      ),
      'file' => 'quiz.admin.inc',
    ),
  );
}

/**
 * Prepares variables for views exposed form templates.
 *
 * Default template: quiz-questions-browser-body.html.twig.
 *
 * @param array $vars
 *   An associative array containing:
 *   - form: A render element representing the form.
 */
function template_preprocess_quiz_questions_browser_body(&$vars) {
  $form =& $vars['form'];
  $rows = array();
  $full_options = array();
  foreach ($form['titles']['#options'] as $key => $value) {
    $full_options[$key] = $form['titles'][$key];
    $full_options[$key]['#title'] = '';
  }

  // We make the question rows
  foreach ($form['titles']['#options'] as $key => $value) {

    // Find nid and vid
    $matches = array();
    preg_match('/([0-9]+)-([0-9]+)/', $key, $matches);
    $quest_nid = $matches[1];
    $quest_vid = $matches[2];

    //Construnct row
    $row = array();
    $row[]['data'] = $full_options[$key];
    $row[]['data'] = l($value, "node/{$quest_nid}", array(
      'html' => TRUE,
      'query' => array(
        'destination' => current_path(),
      ),
      'attributes' => array(
        'target' => 'blank',
      ),
    ));
    $row[]['data'] = $form['types'][$key]['#value'];
    $row[]['data'] = $form['changed'][$key]['#value'];
    $row[]['data'] = $form['names'][$key]['#value'];
    $rows[] = $row;
  }

  //Theme table
  $vars['table'] = array(
    '#theme' => 'table',
    '#header' => array(),
    '#rows' => $rows,
  );
  if (count($form['titles']['#options']) == 0) {
    print t('No questions were found');
  }
}

/**
 * Get the quiz name variable and set it as a constant so we don't have to keep
 * calling it in every function.
 *
 * @return
 *   Quiz name variable.
 */
function _quiz_get_quiz_name() {

  // @todo: remove all the quiz_name stuff? Isn't there better ways to do this?
  return \Drupal::config('quiz.settings')
    ->get('quiz_name');
}

/**
 * Tells whether a question node is being viewed inside/outside of quiz.
 *
 * @return
 *   Boolean.
 */
function _quiz_is_taking_context() {
  return arg(2) == 'take';
}

/**
 * Load a quiz node, cache it and validate that it is indeed of type quiz.
 *
 * @param $arg
 *   The Node ID.
 * @return
 *   A quiz node object or FALSE if a load failed.
 */
function quiz_type_access_load($arg) {
  static $quiz_nodes = array();
  if (isset($quiz_nodes[$arg])) {
    return $quiz_nodes[$arg];
  }
  $to_return = ($node = node_load($arg)) && $node
    ->getType() == 'quiz' ? $node : FALSE;
  if ($to_return) {
    $quiz_nodes[$arg] = $to_return;
  }
  return $to_return;
}

/**
 * Check if this is the last question of the quiz
 */
function _quiz_is_last_question() {
  return count($_SESSION['quiz_' . intval(arg(1))]['quiz_questions']) < 2;
}

/**
 * Retrieve list of question types.
 *
 * @return
 *  Array of question types.
 */
function _quiz_get_question_types() {
  static $to_return = array();

  // We vastly improves performance by statically caching the question types.
  if (!empty($to_return)) {
    return $to_return;
  }

  // Get question types from the modules that defines them..
  $quiz_questions = \Drupal::moduleHandler()
    ->invokeAll('quiz_question_info');

  // Hence module_invoke_all() Deprecated in D8.
  if (empty($quiz_questions)) {
    drupal_set_message(t('You need to install and enable at least one question type(multichoice for instance) to use quiz.'), 'warning', FALSE);
    return array();
  }
  return $quiz_questions;
}

/**
 * Retrieve list of published questions assigned to quiz.
 *
 * This function should be used for question browsers and similiar... It should not be used to decide what questions
 * a user should answer when taking a quiz. quiz_build_question_list is written for that purpose.
 *
 * @param $quiz_nid
 *   Quiz node id.
 * @param $quiz_vid
 *   Quiz node version id.
 * @param $include_all_types
 *   Should the results be filtered on available question types?
 *   @todo: review this.
 * @param $nid_keys
 *   Should nid be used as keys in the array we return?
 * @param $include_question
 *   Should the question(the node body) be included for the questions in the
 *   returned array?
 *
 * @return
 *   An array of questions.
 */
function quiz_get_questions($quiz_nid = NULL, $quiz_vid = NULL, $include_all_types = TRUE, $nid_keys = FALSE, $include_question = TRUE, $include_random = TRUE) {
  $questions = array();
  $query = db_select('node', 'n');
  $query
    ->fields('n', array(
    'nid',
    'type',
  ));
  $query
    ->fields('nr', array(
    'vid',
    'title',
  ));
  $query
    ->fields('qnr', array(
    'question_status',
    'weight',
    'max_score',
    'auto_update_max_score',
  ));
  $query
    ->addField('n', 'vid', 'latest_vid');
  $query
    ->join('node_field_revision', 'nr', 'n.nid = nr.nid');
  $query
    ->join('node_field_data', 'nd', 'n.nid = nd.nid');
  $query
    ->leftJoin('quiz_node_relationship', 'qnr', 'nr.vid = qnr.child_vid');
  if ($include_all_types === TRUE && _quiz_get_question_types()) {
    $query
      ->condition('n.type', array_keys(_quiz_get_question_types()), 'IN');
  }
  if (!is_null($quiz_vid)) {
    $query
      ->condition('parent_vid', $quiz_vid);
    $query
      ->condition('parent_nid', $quiz_nid);
  }
  if ($include_random) {
    $query
      ->condition('question_status', array(
      QUESTION_RANDOM,
      QUESTION_ALWAYS,
    ), 'IN');
  }
  else {
    $query
      ->condition('question_status', QUESTION_ALWAYS);
  }
  $query
    ->condition('nd.status', 1)
    ->orderBy('weight');
  $results = $query
    ->execute();
  foreach ($results as $result) {
    $node = $result;

    // Create questions array.
    if ($nid_keys === FALSE) {

      //while ($node = db_fetch_object($result)) {
      $questions[] = quiz_node_map($node, $include_question);

      //}
    }
    else {

      //while ($node = db_fetch_object($result)) {
      $n = quiz_node_map($node, $include_question);
      $questions[$n
        ->id()] = $n;

      //}
    }
  }
  return $questions;
}

/**
 * Finds out if a quiz has been answered or not.
 *
 * @return
 *   TRUE if there exists answers to the current question.
 */
function quiz_has_been_answered($node) {
  if ($node instanceof \stdClass && !$node->nid || !$node instanceof \stdClass && !$node
    ->id()) {
    return FALSE;
  }
  $query = db_select('quiz_node_results', 'qnr');
  $query
    ->addField('qnr', 'result_id');
  if ($node instanceof \stdClass) {
    $query
      ->condition('nid', $node->nid);
    $query
      ->condition('vid', $node->vid);
  }
  else {
    $query
      ->condition('nid', $node
      ->id());
    $query
      ->condition('vid', $node
      ->getRevisionId());
  }
  $query
    ->range(0, 1);
  return $query
    ->execute()
    ->rowCount() > 0;
}

/**
 * Helper function used when validating integers.
 *
 * @param $value
 *   The value to be validated.
 * @param $min
 *   The minimum value $value is allowed to be.
 * @param $max
 *   The maximum value $value is allowed to be.
 *
 * @return
 *   TRUE if integer in the allowed range. FALSE otherwise.
 */
function _quiz_is_int($value, $min = 1, $max = NULL) {
  $to_return = (string) $value === (string) (int) $value;

  // $value is not an integer.
  if (!$to_return) {
    return FALSE;
  }

  // $value is too small.
  if ($value < $min) {
    return FALSE;
  }

  // $value is too big.
  if (isset($max)) {
    if ($value > $max) {
      return FALSE;
    }
  }

  // $value is an integer in the allowed range.
  return TRUE;
}

/**
 * Map node properties to a question object.
 *
 * @param $node
 *  The question node.
 * @param $include_question
 *  Should the question(the question nodes body) be included?
 *
 * @return
 *  Question object.
 */
function quiz_node_map($node, $include_question = TRUE) {
  $new_question = new \stdClass();
  if ($include_question) {
    $new_question->question = check_markup($node->body, $node->body['und'][0]['format']);
  }
  $new_question->title = $node->title;
  $new_question->nid = $node->nid;
  $new_question->vid = $node->vid;
  $new_question->type = $node->type;
  $new_question->latest_vid = $node->latest_vid;
  $new_question->question_status = isset($node->question_status) ? $node->question_status : QUESTION_NEVER;
  if (isset($node->max_score)) {
    $new_question->max_score = $node->max_score;
  }
  if (isset($node->auto_update_max_score)) {
    $new_question->auto_update_max_score = $node->auto_update_max_score;
  }
  $new_question->weight = $node->weight;
  return $new_question;
}

/**
 * Modify result of option-specific updates.
 *
 * @param $node
 *   The quiz node.
 */
function _quiz_update_resultoptions($node) {

  // Brute force method. Easier to get correct, and probably faster as well.
  db_delete('quiz_node_result_options')
    ->condition('vid', $node
    ->getRevisionId())
    ->execute();
  _quiz_insert_resultoptions($node);
}

/**
 * Updates the max_score property on the specified quizzes
 *
 * @param $quizzes_to_update
 *  Array with the vid's of the quizzes to update
 */
function quiz_update_max_score_properties($quizzes_to_update) {
  if (empty($quizzes_to_update)) {
    return;
  }
  db_update('quiz_node_properties')
    ->expression('max_score', 'max_score_for_random * number_of_random_questions + (
      SELECT COALESCE(SUM(max_score), 0)
      FROM {quiz_node_relationship} qnr
      WHERE qnr.question_status = ' . QUESTION_ALWAYS . '
      AND parent_vid = {quiz_node_properties}.vid)')
    ->condition('vid', $quizzes_to_update, 'IN')
    ->execute();
  db_update('quiz_node_properties')
    ->expression('max_score', '(SELECT COALESCE(SUM(qt.max_score * qt.number), 0)
      FROM {quiz_terms} qt
      WHERE qt.nid = {quiz_node_properties}.nid AND qt.vid = {quiz_node_properties}.vid)')
    ->condition('randomization', 3)
    ->condition('vid', $quizzes_to_update, 'IN')
    ->execute();
  db_update('node_field_revision')
    ->fields(array(
    'created' => REQUEST_TIME,
  ))
    ->condition('vid', $quizzes_to_update, 'IN')
    ->execute();
  db_update('node_field_data')
    ->fields(array(
    'changed' => REQUEST_TIME,
  ))
    ->condition('vid', $quizzes_to_update, 'IN')
    ->execute();
  $results_to_update = db_query('SELECT vid FROM {quiz_node_properties} WHERE vid IN (:vid) AND max_score <> :max_score', array(
    ':vid' => $quizzes_to_update,
    ':max_score' => 0,
  ))
    ->fetchCol();
  if (!empty($results_to_update)) {
    db_update('quiz_node_results')
      ->expression('score', 'ROUND(
	  100 * (
	    SELECT COALESCE (SUM(a.points_awarded), 0)
	    FROM {quiz_node_results_answers} a
	    WHERE a.result_id = {quiz_node_results}.result_id
	  ) / (
	    SELECT max_score
	    FROM {quiz_node_properties} qnp
	    WHERE qnp.vid = {quiz_node_results}.vid
	  )
	)')
      ->condition('vid', $results_to_update, 'IN')
      ->execute();
  }
}

/**
 * Limit the year options to the years 1970 - 2030 for form items of type date.
 *
 * Some systems don't support all the dates the forms api lets you choose from.
 * This function limits the options to dates most systems support.
 *
 * @param $form_element
 *   Form element of type date.
 *
 * @return
 *   Form element with a more limited set of years to choose from.
 */
function _quiz_limit_year_options($form_element) {
  $form_element['year']['#options'] = drupal_map_assoc(range(1970, 2030));
  return $form_element;
}

/**
 * Returns the users default settings.
 *
 * @param $node
 *   Quiz node.
 * @param $uid
 *   (optional) The uid of the user to get the settings for. Defaults to the
 *   current user (NULL).
 *
 * @return
 *   An array of settings. The array is empty in case no settings are available.
 */
function _quiz_load_user_settings($uid = NULL) {

  // The def_uid property is the default user id. It is used if there are no
  // settings store for the current user.
  $uid = isset($uid) ? $uid : \Drupal::currentUser()
    ->id();
  $query = db_select('quiz_user_settings', 'qus')
    ->fields('qus')
    ->condition('uid', $uid);
  $res = $query
    ->execute()
    ->fetchAssoc();
  if (!empty($res)) {
    foreach ($res as $key => $value) {
      if (!in_array($key, array(
        'nid',
        'vid',
        'uid',
      ))) {
        $settings[$key] = $value;
      }
    }

    // TODO : Reviews this later.
    $settings['resultoptions'][] = db_select('quiz_node_result_options', 'qnro')
      ->fields('qnro')
      ->condition('nid', $res['nid'])
      ->condition('vid', $res['vid'])
      ->execute()
      ->fetchAll();
    return $settings;
  }
  return array();
}

/**
 * Returns default values for all quiz settings.
 *
 * @return
 *   Array of default values.
 */
function _quiz_get_node_defaults($new_node = FALSE) {
  return array(
    'aid' => NULL,
    'number_of_random_questions' => 0,
    'max_score_for_random' => 1,
    'pass_rate' => 75,
    'summary_pass' => '',
    'summary_pass_format' => filter_fallback_format(),
    'summary_default' => '',
    'summary_default_format' => filter_fallback_format(),
    'randomization' => 0,
    'backwards_navigation' => 1,
    'repeat_until_correct' => 0,
    'feedback_time' => 0,
    'display_feedback' => 1,
    'quiz_open' => 0,
    'quiz_close' => 0,
    'takes' => 0,
    'show_attempt_stats' => 1,
    'keep_results' => 2,
    'time_limit' => 0,
    'quiz_always' => 1,
    'tid' => 0,
    'has_userpoints' => 0,
    'allow_skipping' => 1,
    'allow_resume' => 1,
    'allow_jumping' => 0,
    'show_passed' => 1,
    'quiz_open' => _quiz_form_prepare_date(),
    'quiz_close' => _quiz_form_prepare_date(NULL, \Drupal::config('quiz.settings')
      ->get('quiz_default_close')),
    'mark_doubtful' => 0,
  );
}

/**
 * Takes a time element and prepares to send it to form_date().
 *
 * @param $time
 *   The time to be turned into an array. This can be:
 *   - A timestamp when from the database.
 *   - An array (day, month, year) when previewing.
 *   - NULL for new nodes.
 *
 * @return
 *   An array for form_date (day, month, year).
 */
function _quiz_form_prepare_date($time = '', $offset = 0) {

  // If this is empty, get the current time.
  if ($time == '') {
    $time = REQUEST_TIME + $offset * 86400;
  }

  // If we are previewing, $time will be an array so just pass it through.
  $time_array = array();
  if (is_array($time)) {
    $time_array = $time;
  }
  elseif (is_numeric($time)) {
    $time_array = array(
      'day' => _quiz_date('j', $time),
      'month' => _quiz_date('n', $time),
      'year' => _quiz_date('Y', $time),
    );
  }
  return $time_array;
}

/**
 * Fetch settings from a node and save them as the users default settings.
 *
 * @param $node
 *   Quiz node.
 */
function _quiz_save_user_settings($node) {
  $user = \Drupal::currentUser();

  //$node = (object) $node;

  // We do not save settings if the node has been created by the system,
  // or if the user haven't requested it
  if (isset($node->auto_created) || !isset($node->remember_settings) || !$node->remember_settings) {
    return FALSE;
  }
  $summary_pass_format = filter_fallback_format();
  if (isset($node->summary_pass['format']) && !empty($node->summary_pass['format'])) {
    $summary_pass_format = $node->summary_pass['format'];
  }
  $summary_default_format = filter_fallback_format();
  if (isset($node->summary_default['format']) && !empty($node->summary_default['format'])) {
    $summary_default_format = $node->summary_default['format'];
  }
  db_merge('quiz_user_settings')
    ->key(array(
    'uid' => $user
      ->id(),
  ))
    ->fields(array(
    'uid' => $node
      ->id() ? $node
      ->id() : $node->save_def_uid,
    'nid' => $node
      ->id(),
    'vid' => $node
      ->getRevisionId(),
    'aid' => isset($node->aid) ? $node->aid : 0,
    'pass_rate' => $node->pass_rate,
    'summary_pass' => isset($node->summary_pass['value']) ? $node->summary_pass['value'] : '',
    'summary_pass_format' => $summary_pass_format,
    'summary_default' => $node->summary_default['value'],
    'summary_default_format' => $summary_default_format,
    'randomization' => $node->randomization,
    'backwards_navigation' => $node->backwards_navigation,
    'keep_results' => $node->keep_results,
    'repeat_until_correct' => $node->repeat_until_correct,
    'feedback_time' => $node->feedback_time,
    'display_feedback' => $node->display_feedback,
    'takes' => $node->takes,
    'show_attempt_stats' => $node->show_attempt_stats,
    'time_limit' => isset($node->time_limit) ? $node->time_limit : 0,
    'quiz_always' => $node->quiz_always,
    'has_userpoints' => isset($node->has_userpoints) ? $node->has_userpoints : 0,
    'allow_skipping' => $node->allow_skipping,
    'allow_resume' => $node->allow_resume,
    'allow_jumping' => $node->allow_jumping,
    'show_passed' => $node->show_passed,
  ))
    ->execute();
  drupal_set_message(t('Default settings have been saved'));
}

/**
 * Copies questions when a quiz is translated.
 *
 * @param $node
 *   The new translated quiz node.
 */
function quiz_copy_questions($node) {

  // Find original questions.
  $query = db_query('SELECT child_nid, child_vid, question_status, weight, max_score, auto_update_max_score FROM {quiz_node_relationship}
          WHERE parent_vid = :parent_vid', array(
    ':parent_vid' => $node->translation_source->vid,
  ));
  foreach ($query as $res_o) {
    $original_question = node_load($res_o->child_nid);

    // Set variables we can't or won't carry with us to the translated node to
    // NULL.
    $original_question->nid = $original_question->vid = $original_question->created = $original_question->changed = NULL;
    $original_question->revision_timestamp = $original_question->menu = $original_question->path = NULL;
    $original_question->files = array();
    if (isset($original_question->book['mlid'])) {
      $original_question->book['mlid'] = NULL;
    }

    // Set the correct language.
    $original_question->language = $node->language;

    // Save the node.
    $original_question
      ->save();

    // Save the relationship between the new question and the quiz.
    $sql = 'INSERT INTO {quiz_node_relationship}
            (parent_nid, parent_vid, child_nid, child_vid, question_status, weight, max_score, auto_update_max_score)
            VALUES(:parent_nid, :parent_vid, :child_nid, :child_vid, :question_status, :weight, :max_score, :auto_update_max_score)';
    db_query($sql, array(
      ':parent_nid' => $node->nid,
      ':parent_vid' => $node->vid,
      ':child_nid' => $original_question->nid,
      ':child_vid' => $original_question->vid,
      ':question_status' => $res_o->question_status,
      ':weight' => $res_o->weight,
      ':max_score' => $res_o->max_score,
      ':auto_update_max_score' => $res_o->auto_update_max_score,
    ));
  }
}

/**
 * Common actions that need to be done before a quiz is inserted or updated
 *
 * @param $node
 *   Quiz node
 */
function _quiz_common_presave_actions(&$node) {
  quiz_translate_form_date($node, 'quiz_open');
  quiz_translate_form_date($node, 'quiz_close');
  if (empty($node->pass_rate)) {
    $node->pass_rate = 0;
  }
  if ($node->randomization < 2) {
    $node->number_of_random_questions = 0;
  }
}

/**
 * If a quiz is saved as not randomized we should make sure all random questions
 * are converted to always.
 *
 * @param $node
 *   Quiz node.
 */
function _quiz_check_num_random(&$node) {
  if ($node->randomization == 2) {
    return;
  }
  db_delete('quiz_node_relationship')
    ->condition('question_status', QUESTION_RANDOM)
    ->condition('parent_vid', $node
    ->getRevisionId())
    ->execute();
}

/**
 * If a quiz is saved with random categories we should make sure all questions
 * are removed from the quiz
 *
 * @param $node
 *   Quiz node.
 */
function _quiz_check_num_always(&$node) {
  if ($node->randomization != 3) {
    return;
  }
  db_delete('quiz_node_relationship')
    ->condition('parent_vid', $node->vid)
    ->execute();
}

/**
 * Get the number of compulsory questions for a quiz.
 *
 * @param $nid
 *   Quiz node id.
 * @param $vid
 *   Quiz node version id.
 * @return
 *   Number of compulsory questions.
 */
function _quiz_get_num_always_questions($vid) {
  return db_query('SELECT COUNT(*) FROM {quiz_node_relationship} qnr
          JOIN {node} n ON n.nid = qnr.child_nid
          JOIN {node_field_data} nd ON nd.nid = qnr.child_nid
          WHERE nd.status=1 AND qnr.parent_vid = :parent_vid AND qnr.question_status = :question_status', array(
    ':parent_vid' => $vid,
    ':question_status' => QUESTION_ALWAYS,
  ))
    ->fetchField();
}

/**
 * Find out if a quiz is available for taking or not
 *
 * @param $quiz
 *  The quiz node
 * @return
 *  TRUE if available
 *  Error message(String) if not available
 */
function quiz_availability(EntityInterface $quiz) {
  $user = \Drupal::currentUser();
  if ($user
    ->id() == 0 && $quiz->takes > 0) {
    return t('This quiz only allows %num_attempts attempts. Anonymous users can only access quizzes that allows an unlimited number of attempts.', array(
      '%num_attempts' => $quiz->takes,
    ));
  }
  $user_is_admin = $user
    ->hasPermission('edit any quiz content') || $user
    ->hasPermission('edit own quiz content') && $quiz
    ->id() == $user
    ->id();
  if ($user_is_admin || $quiz->quiz_always == 1) {
    return TRUE;
  }

  // Compare current GMT time to the open and close dates (which should still be
  // in GMT time).
  $now = gmmktime();
  if ($now >= $quiz->quiz_close || $now < $quiz->quiz_open) {
    return t('This quiz is closed');
  }
  return TRUE;
}

/**
 * Helper function for hook_view().
 *
 * Returns a button to use as a link to start taking the quiz.
 *
 * @param $form_state
 *   Form state array.
 * @param $node
 *   The quiz node.
 * @return
 *   Form with a button linking to the take tab.
 */
function quiz_start_quiz_button_form($form, &$form_state, EntityInterface $node) {
  $form = array();
  $form['#action'] = url("node/" . $node
    ->id() . "/take");
  $form['button'] = array(
    '#type' => 'button',
    '#value' => t('Start quiz'),
  );
  return $form;
}

/**
 * Get an array of feedback options.
 *
 * @return
 *   An array of feedback options.
 */
function _quiz_get_feedback_options() {
  return array(
    QUIZ_FEEDBACK_END => t('At the end of the @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
    QUIZ_FEEDBACK_QUESTION => t('After each question'),
    QUIZ_FEEDBACK_NEVER => t('Do not show'),
  );
}

/**
 * Insert call specific to result options.
 *
 * This is called by quiz_insert().
 *
 * @param $node
 *   The quiz node.
 */
function _quiz_insert_resultoptions($node) {
  if (!isset($node->resultoptions)) {
    return;
  }
  $query = db_insert('quiz_node_result_options')
    ->fields(array(
    'nid',
    'vid',
    'option_name',
    'option_summary',
    'option_summary_format',
    'option_start',
    'option_end',
  ));
  foreach ($node->resultoptions as $id => $option) {
    if (!empty($option['option_name'])) {

      // When this function called direct from node form submit the $option['option_summary']['value'] and $option['option_summary']['format'] are we need
      // But when updating a quiz node eg. on manage questions page, this values come from loaded node, not from a submitted form.
      if (is_array($option['option_summary'])) {
        $option['option_summary_format'] = $option['option_summary']['format'];
        $option['option_summary'] = $option['option_summary']['value'];
      }
      $query
        ->values(array(
        'nid' => $node
          ->id(),
        'vid' => $node
          ->getRevisionId(),
        'option_name' => $option['option_name'],
        'option_summary' => $option['option_summary'],
        'option_summary_format' => $option['option_summary_format'],
        'option_start' => $option['option_start'],
        'option_end' => $option['option_end'],
      ));
    }
  }
  $query
    ->execute();
}

/*
 *  Implements hook_node_insert().
 */
function quiz_node_insert(EntityInterface $node) {
  if ($node
    ->getType() == 'quiz') {

    // Apply defaults.
    $defaults = _quiz_load_user_settings(\Drupal::config('quiz.settings')
      ->get('quiz_def_uid')) + _quiz_get_node_defaults(TRUE);
    foreach ($defaults as $key => $value) {
      if (!isset($node->{$key})) {
        $node->{$key} = $value;
      }
    }
    _quiz_save_user_settings($node);

    // Copy all the questions belonging to the quiz if this is a new translation.
    if ($node
      ->isNew() && isset($node->translation_source)) {
      quiz_copy_questions($node);
    }
    _quiz_common_presave_actions($node);

    // If the quiz is saved as not randomized we have to make sure that questions belonging to the quiz are saved as not random
    _quiz_check_num_random($node);
    _quiz_check_num_always($node);
    db_insert('quiz_node_properties')
      ->fields(array(
      'vid' => $node
        ->getRevisionId(),
      'nid' => $node
        ->id(),
      'aid' => !empty($node->aid) ? $node->aid : 0,
      'number_of_random_questions' => $node->number_of_random_questions,
      'randomization' => $node->randomization,
      'backwards_navigation' => $node->backwards_navigation,
      'repeat_until_correct' => $node->repeat_until_correct,
      'quiz_open' => strtotime('now'),
      //TODO:// verify with strtotime($node->quiz_open),
      'quiz_close' => strtotime('+1 month'),
      //TODO:// verify with strtotime($node->quiz_close),
      'takes' => $node->takes,
      'show_attempt_stats' => $node->show_attempt_stats,
      'keep_results' => $node->keep_results,
      'time_limit' => $node->time_limit,
      'pass_rate' => $node->pass_rate,
      'summary_pass' => is_array($node->summary_pass) ? $node->summary_pass['value'] : $node->summary_pass,
      'summary_pass_format' => is_array($node->summary_pass) ? $node->summary_pass['format'] : $node->summary_pass_format,
      'summary_default' => is_array($node->summary_default) ? $node->summary_default['value'] : $node->summary_default,
      'summary_default_format' => is_array($node->summary_default) ? $node->summary_default['format'] : $node->summary_default_format,
      'quiz_always' => $node->quiz_always,
      'feedback_time' => $node->feedback_time,
      'display_feedback' => $node->display_feedback,
      'tid' => isset($node->tid) ? $node->tid : 0,
      'has_userpoints' => isset($node->has_userpoints) ? $node->has_userpoints : 0,
      'allow_skipping' => $node->allow_skipping,
      'allow_resume' => $node->allow_resume,
      'allow_jumping' => $node->allow_jumping,
      'show_passed' => $node->show_passed,
      'mark_doubtful' => $node->mark_doubtful,
    ))
      ->execute();
    _quiz_insert_resultoptions($node);
  }
}

/**
 * Implements hook_node_load().
 */
function quiz_node_load($nodes) {
  foreach ($nodes as $nid => $node) {
    if ($node
      ->getType() == 'quiz') {

      // Fetching defaults.
      $default_additions = _quiz_get_node_defaults();

      // Fetching node settings.
      $query = db_select('quiz_node_properties', 'qnp');
      foreach (array_keys($default_additions) as $field) {
        $query
          ->addField('qnp', $field);
      }
      $query
        ->condition('vid', $node
        ->getRevisionId());
      $query
        ->condition('nid', $node
        ->id());
      $fetched_additions = (array) $query
        ->execute()
        ->fetch();
      $additions = $fetched_additions ? (object) ($fetched_additions += $default_additions) : NULL;

      // Fetching result options.
      $query = db_select('quiz_node_result_options', 'qnro');
      $query
        ->fields('qnro');
      $query
        ->condition('nid', $node
        ->id());
      $query
        ->condition('vid', $node
        ->getRevisionId());
      $options = $query
        ->execute();
      foreach ($options as $option) {
        $additions->resultoptions[$option->option_id] = (array) $option;
      }
      foreach ($additions as $property => &$value) {
        $node->{$property} = $value;
      }
    }
  }
}

/**
 * Implements hook_node_view().
 */
function quiz_node_view(EntityInterface $node, EntityDisplay $display, $view_mode, $langcode) {
  if ($node
    ->getType() == 'quiz') {

    // drupal_alter() is deprecated in D8.
    \Drupal::moduleHandler()
      ->alter('quiz_view', $node, $teaser, $page);

    //TODO: Need to verify

    //node_invoke($node, 'prepare');

    //\Drupal::moduleHandler()->invoke('quiz', 'node_prepare_form', array($node));

    // Number of questions is needed on the statistics page.
    $node->number_of_questions = $node->number_of_random_questions + _quiz_get_num_always_questions($node
      ->getRevisionId());
    $node->content['stats'] = array(
      '#markup' => theme('quiz_view_stats', array(
        'node' => $node,
      )),
      '#weight' => 0,
    );
    $available = quiz_availability($node);
    if ($available === TRUE) {

      // Check the permission before displaying start button.
      if (\Drupal::currentUser()
        ->hasPermission('access quiz')) {

        // Add a link to the take tab as a button if this isn't a teaser view.
        if (!$teaser) {
          $quiz_form = drupal_get_form('quiz_start_quiz_button_form', $node);
          $node->content['take'] = array(
            '#markup' => drupal_render($quiz_form),
            '#weight' => 2,
          );
        }
        else {
          $node->content['take'] = array(
            '#markup' => l(t('Start quiz'), 'node/' . $node
              ->id() . '/take'),
            '#weight' => 2,
          );
        }
      }
    }
    else {
      $node->content['take'] = array(
        '#markup' => '<div class="quiz-not-available">' . $available . '</div>',
        '#weight' => 2,
      );
    }
  }
}

/**
 * Implements hook_node_delete().
 */
function quiz_node_delete(EntityInterface $node) {
  if ($node
    ->getType() == 'quiz') {
    $res = db_query('SELECT result_id FROM {quiz_node_results}
          WHERE nid = :nid', array(
      ':nid' => $node
        ->id(),
    ));
    $rids = array();
    while ($rid = $res
      ->fetchField()) {
      $rids[] = $rid;
    }
    quiz_delete_results($rids);

    // Remove quiz node records from table quiz_node_properties
    db_delete('quiz_node_properties')
      ->condition('nid', $node
      ->id())
      ->execute();

    // Remove quiz node records from table quiz_node_relationship
    db_delete('quiz_node_relationship')
      ->condition('parent_nid', $node
      ->id())
      ->execute();

    // Remove quiz node records from table quiz_node_results
    db_delete('quiz_node_results')
      ->condition('nid', $node
      ->id())
      ->execute();

    // Remove quiz node records from table quiz_node_result_options
    db_delete('quiz_node_result_options')
      ->condition('nid', $node
      ->id())
      ->execute();
  }
}

/**
 * Makes, saves and returns a new quiz node.
 *
 * @param $title
 *   The title of the new node.
 *
 * @return
 *   New quiz node object.
 */
function quiz_make_new($title) {
  $user = \Drupal::currentUser();
  $new_node = array();

  // Get default user settings.
  $settings = _quiz_load_user_settings();
  if (!$settings) {
    $settings = _quiz_load_user_settings(\Drupal::config('quiz.settings')
      ->get('quiz_def_uid'));
  }
  $quiz_default = _quiz_get_node_defaults();

  //TODO: Find right way to save 'quiz_open' and 'quiz_close'
  if (isset($quiz_default['quiz_open'])) {
    $quiz_default['quiz_open'] = strtotime($quiz_default['quiz_open']);
  }
  if (isset($quiz_default['quiz_close'])) {
    $quiz_default['quiz_close'] = strtotime($quiz_default['quiz_close']);
  }
  $settings += $quiz_default;
  foreach ($settings as $key => $value) {
    $new_node[$key] = $value;
  }
  $new_node['uid'] = $user
    ->id();
  $new_node['type'] = 'quiz';
  $new_node['title'] = $title;
  $new_node['status'] = 1;
  $new_node['auto_created'] = TRUE;
  $node = entity_create('node', $new_node);
  $node
    ->save();
  if (is_numeric($node
    ->id())) {
    drupal_set_message(t('Quiz %title has been created.', array(
      '%title' => $title,
    )));
  }
  return $node;
}

/**
 * Sets the questions that are assigned to a quiz.
 *
 * @param $quiz
 *   The quiz(node) to modify.
 * @param $questions
 *   An array of questions.
 * @param $set_new_revision
 *   If TRUE, a new revision will be generated. Note that saving
 *   quiz questions unmodified will still generate a new revision of the quiz if
 *   this is set to TRUE. Why? For a few reasons:
 *   - All of the questions are updated to their latest VID. That is supposed to
 *     be a feature.
 *   - All weights are updated.
 *   - All status flags are updated.
 *
 * @return
 *   Boolean TRUE if update was successful, FALSE otherwise.
 */
function quiz_set_questions(&$quiz, $questions, $set_new_revision = FALSE) {
  $old_vid = $quiz
    ->getRevisionId();
  if ($set_new_revision) {

    // Create a new Quiz VID, even if nothing changed.
    $quiz->revision = 1;
  }
  $quiz
    ->save();

  // When node_save() calls all of the node API hooks, old quiz info is
  // automatically inserted into quiz_node_relationship. We could get clever and
  // try to do strategic updates/inserts/deletes, but that method has already
  // proven error prone as the module has gained complexity (See 5.x-2.0-RC2).
  // So we go with the brute force method:
  db_delete('quiz_node_relationship')
    ->condition('parent_nid', $quiz
    ->id())
    ->condition('parent_vid', $quiz
    ->getRevisionId())
    ->execute();
  if (empty($questions)) {
    return TRUE;

    // This is not an error condition.
  }
  $qnr_insert = db_insert('quiz_node_relationship')
    ->fields(array(
    'parent_nid',
    'parent_vid',
    'child_nid',
    'child_vid',
    'question_status',
    'weight',
    'max_score',
    'auto_update_max_score',
  ));
  foreach ($questions as $question) {
    if ($question->state != QUESTION_NEVER) {
      $qnr_insert
        ->values(array(
        'parent_nid' => $quiz
          ->id(),
        'parent_vid' => $quiz
          ->getRevisionId(),
        'child_nid' => $question->nid,
        'child_vid' => $question->refresh ? db_query('SELECT vid FROM {node} WHERE nid = :nid', array(
          ':nid' => $question->nid,
        ))
          ->fetchField() : $question->vid,
        'question_status' => $question->state,
        'weight' => $question->weight,
        'max_score' => (int) $question->max_score,
        'auto_update_max_score' => (int) $question->auto_update_max_score,
      ));
    }
  }
  if (count($questions)) {
    $qnr_insert
      ->execute();
  }
  quiz_update_max_score_properties(array(
    $quiz
      ->getRevisionId(),
  ));
  return TRUE;
}

/**
 * Does the current user have access to take the quiz?
 *
 * @param $node
 *  The quiz node
 */
function quiz_take_access($node) {
  if ($node
    ->getType() != 'quiz') {
    return FALSE;
  }
  return node_access('view', $node) && \Drupal::currentUser()
    ->hasPermission('access quiz') && quiz_availability($node) === TRUE;
}

/**
 * Copies quiz-question relation entries in the quiz_node_relationship table
 * from an old version of a quiz to a new.
 *
 * @param $old_quiz_vid
 *   The quiz vid prior to a new revision.
 * @param $new_quiz_vid
 *   The quiz vid of the latest revision.
 * @param $quiz_nid
 *   The quiz node id.
 */
function quiz_update_quiz_question_relationship($old_quiz_vid, $new_quiz_vid, $quiz_nid) {

  // query for questions in previous version
  $result = db_select('quiz_node_relationship', 'qnr')
    ->fields('qnr', array(
    'parent_nid',
    'child_nid',
    'child_vid',
    'question_status',
    'weight',
    'max_score',
    'auto_update_max_score',
  ))
    ->condition('parent_nid', $quiz_nid)
    ->condition('parent_vid', $old_quiz_vid)
    ->condition('question_status', QUESTION_NEVER, '!=')
    ->execute();

  // only proceed if query returned data
  if ($result
    ->rowCount()) {
    $insert_query = db_insert('quiz_node_relationship')
      ->fields(array(
      'parent_nid',
      'parent_vid',
      'child_nid',
      'child_vid',
      'question_status',
      'weight',
      'max_score',
      'auto_update_max_score',
    ));
    while ($quiz_question = $result
      ->fetchAssoc()) {
      $insert_query
        ->values(array(
        'parent_nid' => $quiz_nid,
        'parent_vid' => $new_quiz_vid,
        'child_nid' => $quiz_question['child_nid'],
        'child_vid' => $quiz_question['child_vid'],
        'question_status' => $quiz_question['question_status'],
        'weight' => $quiz_question['weight'],
        'max_score' => $quiz_question['max_score'],
        'auto_update_max_score' => $quiz_question['auto_update_max_score'],
      ));
    }
    $insert_query
      ->execute();
  }

  /* Update terms if any */
  $result = db_select('quiz_terms', 'qt')
    ->fields('qt', array(
    'nid',
    'tid',
    'weight',
    'max_score',
    'number',
  ))
    ->condition('vid', $old_quiz_vid)
    ->execute();

  // only proceed if query returned data
  if ($result
    ->rowCount()) {
    $insert_query = db_insert('quiz_terms')
      ->fields(array(
      'nid',
      'vid',
      'tid',
      'weight',
      'max_score',
      'number',
    ));
    while ($quiz_term = $result
      ->fetchAssoc()) {
      $insert_query
        ->values(array(
        'nid' => $quiz_nid,
        'vid' => $new_quiz_vid,
        'tid' => $quiz_term['tid'],
        'weight' => $quiz_term['weight'],
        'max_score' => $quiz_term['max_score'],
        'number' => $quiz_term['number'],
      ));
    }
    $insert_query
      ->execute();
  }

  /*$sql = "INSERT INTO {quiz_terms} (nid, vid, tid, weight, max_score, number)
    SELECT qt.nid, %d, qt.tid, qt.weight, qt.max_score, qt.number
    FROM {quiz_terms} qt
    WHERE qt.vid = %d";*/
}

/**
 * Handles quiz taking.
 *
 * This gets executed when the main quiz node is first loaded.
 *
 * @param $quiz
 *   The quiz node.
 *
 * @return
 *   Content array.
 */
function quiz_take_quiz(NodeInterface $quiz) {
  $user = \Drupal::currentUser();
  $allow_skipping = $quiz->allow_skipping;
  if (!isset($quiz)) {
    throw new NotFoundHttpException();

    // Equivalent to drupal_not_found() in D7;
  }

  // If anonymous user and no unique hash, refresh with a unique string to
  // prevent caching.
  if (!$user
    ->id() && arg(4) != NULL) {
    return new RedirectResponse(url('node/' . $quiz
      ->id() . '/take/' . md5(mt_rand() . time()), array(
      'absolute' => TRUE,
    )));
  }

  // Make sure we use the same revision of the quiz throughout the quiz taking
  // session.
  if (isset($_SESSION['quiz_' . $quiz
    ->id()]['quiz_vid']) && $quiz
    ->getRevisionId() != $_SESSION['quiz_' . $quiz
    ->id()]['quiz_vid'] && \Drupal::config('quiz.settings')
    ->get('quiz_auto_revisioning')) {
    $quiz = node_load($quiz
      ->id(), $_SESSION['quiz_' . $quiz
      ->id()]['quiz_vid']);
  }

  // If the session has no data for this quiz.
  if (!isset($_SESSION['quiz_' . $quiz
    ->id()]['quiz_questions'])) {

    // We delete questions in progress from old revisions.
    _quiz_delete_old_in_progress($quiz, $user
      ->id());

    // See if the current user has progress for this revision of the quiz stored
    // in the database
    $rid = $user
      ->id() > 0 ? _quiz_active_result_id($user
      ->id(), $quiz
      ->id(), $quiz
      ->getRevisionId()) : 0;

    // Are we resuming an in-progress quiz?
    if ($quiz->allow_resume && $rid > 0) {
      _quiz_resume_existing_quiz($quiz, $user
        ->id(), $rid);
    }
    elseif (quiz_start_check($quiz, $rid)) {
      _quiz_take_quiz_init($quiz);
    }
    else {
      return array(
        'body' => array(
          '#markup' => t('This quiz is closed'),
        ),
      );
    }
  }
  $q_passed_validation = FALSE;
  if (quiz_availability($quiz) !== TRUE) {
    drupal_set_message(t('This quiz is not available anymore.'), 'error');
    return array(
      'body' => array(
        '#markup' => t('This quiz is closed'),
      ),
    );
  }
  if (isset($_SESSION['quiz_' . $quiz
    ->id()]['question_duration'])) {
    $_SESSION['quiz_' . $quiz
      ->id()]['question_duration'] -= REQUEST_TIME - $_SESSION['quiz_' . $quiz
      ->id()]['question_start_time'];
  }
  if (!isset($_POST['op'])) {

    // @todo Starting new quiz... Do we need to show instructions here?
  }
  elseif (isset($_POST['question_nid']) && $_POST['question_nid'] != $_SESSION['quiz_' . $quiz
    ->id()]['quiz_questions'][0]['nid']) {

    // The user has pressed the navigation buttons multiple times...
  }
  elseif (isset($_SESSION['quiz_' . $quiz
    ->id()]['question_duration']) && $_SESSION['quiz_' . $quiz
    ->id()]['question_duration'] < -2) {

    // Timed quiz where the time has gone out 2 seconds ago. Do not store the users results...
  }
  elseif ($_POST['op'] == t('Next question')) {
    $url = "node/{$quiz->id()}/take";
    return new RedirectResponse($url);
  }
  elseif ($_POST['op'] == t('Finish') || $_POST['op'] == t('Next') || $_POST['op'] == t('Back') && $quiz->backwards_navigation) {

    // Previous quiz questions: Questions that have been asked already. We save
    // a record of all of them so that a user can navigate backward all the way
    // to the beginning of the quiz.
    $_SESSION['quiz_' . $quiz
      ->id()]['quiz_questions'][0]['rid'] = $_SESSION['quiz_' . $quiz
      ->id()]['result_id'];
    $_SESSION['quiz_' . $quiz
      ->id()]['previous_quiz_questions'][] = $_SESSION['quiz_' . $quiz
      ->id()]['quiz_questions'][0];
    $former_question_array = array_shift($_SESSION['quiz_' . $quiz
      ->id()]['quiz_questions']);
    $former_question = node_load($former_question_array['nid'], $former_question_array['vid']);

    // Call hook_evaluate_question().
    $types = _quiz_get_question_types();
    $module = $types[$former_question
      ->getType()]['module'];
    $result = module_invoke($module, 'evaluate_question', $former_question, $_SESSION['quiz_' . $quiz
      ->id()]['result_id']);
    $q_passed_validation = $result->is_valid;
    $check_jump = TRUE;
    if ($q_passed_validation === TRUE) {
      quiz_store_question_result($quiz, $result, array(
        'set_msg' => TRUE,
        'question_data' => $former_question_array,
      ));
    }
    elseif ($quiz->allow_jumping && _quiz_is_int($_POST['jump_to_question'])) {
      $_POST['op'] = t('Leave blank');
      $allow_skipping = TRUE;
      $jumping = TRUE;
    }

    // Stash feedback in the session, since the $_POST gets cleared.
    if ($quiz->feedback_time == QUIZ_FEEDBACK_QUESTION && $_POST['op'] != t('Back') && $q_passed_validation === TRUE) {

      // Invoke hook_get_report().
      $report = module_invoke($module, 'get_report', $former_question_array['nid'], $former_question_array['vid'], $_SESSION['quiz_' . $quiz
        ->id()]['result_id']);
      module_load_include('pages.inc', 'quiz');
      if ($report) {
        $report_form = drupal_get_form('quiz_report_form', array(
          $report,
        ), TRUE, TRUE, TRUE);
        $report_form['op'] = array(
          '#type' => 'submit',
          '#value' => t('Next question'),
        );
        return $report_form;
      }
    }
    if ($quiz->repeat_until_correct && $_POST['op'] != t('Back') && $q_passed_validation === TRUE) {

      // If the question was answered incorrectly, repeat it
      if ($result && !$result->is_correct && $result->is_evaluated) {
        $last_q = array_pop($_SESSION['quiz_' . $quiz
          ->id()]['previous_quiz_questions']);
        array_unshift($_SESSION['quiz_' . $quiz
          ->id()]['quiz_questions'], $last_q);
        drupal_set_message(t('The answer was incorrect. Please try again.'), 'error');
        unset($_SESSION['quiz_' . $quiz
          ->id()]['feedback']);
      }
    }
    elseif ($_POST['op'] == t('Back') && $quiz->backwards_navigation) {
      $quiz_id = 'quiz_' . $quiz
        ->id();

      // We jump back two times. From the next question to the current, and then
      // from the current to the previous.
      for ($i = 0; $i < 2; $i++) {
        $last_q = array_pop($_SESSION[$quiz_id]['previous_quiz_questions']);
        array_unshift($_SESSION[$quiz_id]['quiz_questions'], $last_q);
      }
    }

    // If anonymous user, refresh url with unique hash to prevent caching.
    if (!$user
      ->id() && $q_passed_validation === TRUE) {
      $url = url('node/' . $quiz
        ->id() . '/take', array(
        'query' => array(
          'quizkey' => md5(mt_rand() . REQUEST_TIME),
        ),
      ));
      return new RedirectResponse($url);
    }
  }

  // Check for a skip.
  if (isset($_POST['op']) && ($_POST['op'] == t('Leave blank') || $_POST['op'] == t('Leave blank and finish')) && $allow_skipping) {
    if (!isset($_SESSION['quiz_' . $quiz
      ->id()]['result_id'])) {
      $_SESSION['quiz_' . $quiz
        ->id()]['result_id'] = quiz_create_rid($quiz);
    }
    $q_passed_validation = TRUE;

    // Advance the question.
    if (!isset($jumping) || isset($jumping) && !$jumping) {
      $_SESSION['quiz_' . $quiz
        ->id()]['previous_quiz_questions'][] = $_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions'][0];

      // Load the last asked question.
      $former_question_array = array_shift($_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions']);
      $former_question = node_load($former_question_array['nid'], $former_question_array['vid']);
    }

    // Call hook_skip_question().
    $module = quiz_question_module_for_type($former_question
      ->getType());
    if (!$module) {
      return array(
        'body' => array(
          '#markup' => ' ',
        ),
      );
    }
    $result = module_invoke($module, 'skip_question', $former_question, $_SESSION['quiz_' . $quiz
      ->id()]['result_id']);

    // Store that the question was skipped:
    quiz_store_question_result($quiz, $result, array(
      'set_msg' => TRUE,
      'question_data' => $former_question_array,
    ));
  }
  if (isset($check_jump) && $check_jump) {
    if ($quiz->allow_jumping && _quiz_is_int($_POST['jump_to_question'])) {
      quiz_jump_to($_POST['jump_to_question'], $quiz, $_SESSION['quiz_' . $quiz
        ->id()]['result_id']);
    }
  }
  $show_validation_message = FALSE;

  // If this quiz is in progress, load the next questions and return it via the theme.
  if (!empty($_SESSION['quiz_' . $quiz
    ->id()]['quiz_questions']) || is_string($q_passed_validation)) {

    //TODO:need to verify the condition

    // If we got no error when validating the question
    if (!is_string($q_passed_validation) || $_POST['op'] == t('Back') && $quiz->backwards_navigation) {
      $question_node = node_load($_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions'][0]['nid'], $_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions'][0]['vid']);
      if (isset($_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions'][0]['rid'])) {
        $question_node->rid = $_SESSION['quiz_' . $quiz
          ->id()]['quiz_questions'][0]['rid'];
      }

      // We got an error message when trying to validate the previous answer
    }
    else {
      $question_node = $former_question;
      $show_validation_message = TRUE;
      array_unshift($_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions'], $former_question_array);
      if (is_array($_SESSION['quiz_' . $quiz
        ->id()]['previous_quiz_questions'])) {
        array_pop($_SESSION['quiz_' . $quiz
          ->id()]['previous_quiz_questions']);
      }

      // Avoid caching for anonymous users
      if (!$user
        ->id()) {
        drupal_set_message($q_passed_validation, 'error');
        $url = url('node/' . $quiz
          ->id() . '/take', array(
          'query' => array(
            'quizkey' => md5(mt_rand() . REQUEST_TIME),
          ),
        ));
        return new RedirectResponse($url);
      }
    }

    // Added the progress info to the view.
    $number_of_questions = quiz_get_number_of_questions($quiz
      ->getRevisionId());
    $question_number = $number_of_questions - count($_SESSION['quiz_' . $quiz
      ->id()]['quiz_questions']);
    $question_node->question_number = $question_number;
    $content['progress']['#markup'] = theme('quiz_progress', array(
      'question_number' => $question_number,
      'num_questions' => $number_of_questions,
      'allow_jumping' => $quiz->allow_jumping,
      'time_limit' => $quiz->time_limit,
    ));
    $content['progress']['#weight'] = -50;
    if (count($_SESSION['quiz_' . $quiz
      ->id()]['quiz_questions']) + count($_SESSION['quiz_' . $quiz
      ->id()]['previous_quiz_questions']) > $number_of_questions) {
      drupal_set_message(t('At least one question have been deleted from the quiz after you started taking it. You will have to start over.'), 'warning', FALSE);
      unset($_SESSION['quiz_' . $quiz
        ->id()]);
      $url = url('node/' . $quiz
        ->id() . '/take');
      return new RedirectResponse($url);
    }
    if (isset($_SESSION['quiz_' . $quiz
      ->id()]['question_duration'])) {
      $time = $_SESSION['quiz_' . $quiz
        ->id()]['question_duration'];
      if ($time < 1 && $time > -2) {

        // The page was probably submitted by the js, we allow the data to be stored
        $time = 1;
      }
      db_update('quiz_node_results')
        ->fields(array(
        'time_left' => $time,
      ))
        ->condition('result_id', $_SESSION['quiz_' . $quiz
        ->id()]['result_id'])
        ->execute();
      if ($time <= 1) {

        // Quiz has been timed out, run a loop to mark the remaining questions
        // as skipped.
        quiz_jump_to(count($_SESSION['quiz_' . $quiz
          ->id()]['quiz_questions']) + count($_SESSION['quiz_' . $quiz
          ->id()]['previous_quiz_questions']) + 1, $quiz, $_SESSION['quiz_' . $quiz
          ->id()]['result_id']);
        $quiz_end = TRUE;
        unset($content['progress']);
        $show_validation_message = FALSE;
        drupal_set_message(t('You have run out of time.'), 'error');
      }
      else {

        // There is still time left, so let's go ahead and insert the countdown
        // javascript.
        if (function_exists('jquery_countdown_add') && \Drupal::config('quiz.settings')
          ->get('quiz_has_timer')) {
          jquery_countdown_add('.countdown', array(
            'until' => $time,
            'onExpiry' => 'finished',
            'compact' => TRUE,
            'layout' => t('Time left') . ': {hnn}{sep}{mnn}{sep}{snn}',
          ));

          // These are the two button op values that are accepted for answering
          // questions.
          $button_op1 = drupal_json_encode(t('Finish'));
          $button_op2 = drupal_json_encode(t('Next'));
          $js = "\n            function finished() {\n              // Find all buttons with a name of 'op'.\n              var buttons = jQuery('input[type=submit][name=op], button[type=submit][name=op]');\n              // Filter out the ones that don't have the right op value.\n              buttons = buttons.filter(function() {\n                return this.value == {$button_op1} || this.value == {$button_op2};\n              });\n              if (buttons.length == 1) {\n                // Since only one button was found, this must be it.\n                buttons.click();\n              }\n              else {\n                // Zero, or more than one buttons were found; fall back on a page refresh.\n                window.location = window.location.href;\n              }\n            }\n          ";
          drupal_add_js($js, array(
            'type' => 'inline',
            'scope' => JS_DEFAULT,
          ));
        }
      }
      $_SESSION['quiz_' . $quiz
        ->id()]['question_start_time'] = REQUEST_TIME;
    }
    if ($show_validation_message) {
      drupal_set_message($q_passed_validation, 'error');
    }

    // If we're not yet at the end.
    if (empty($quiz_end)) {
      $content['body']['question']['#markup'] = quiz_take_question_view($question_node, $quiz);
      $content['body']['question']['#weight'] = 0;

      // If we had feedback from the last question.
      if (isset($_SESSION['quiz_' . $quiz
        ->id()]['feedback']) && $quiz->feedback_time == QUIZ_FEEDBACK_QUESTION) {
        $content['body']['feedback']['#markup'] = rawurldecode($_SESSION['quiz_' . $quiz
          ->id()]['feedback']);
        $content['body']['feedback']['#weight'] = -100;
      }
      drupal_set_title($quiz
        ->getTitle());
      unset($_SESSION['quiz_' . $quiz
        ->id()]['feedback']);
    }
  }
  else {
    drupal_set_title($quiz
      ->getTitle());
    $quiz_end = TRUE;
  }

  // If we're at the end of the quiz.
  if (!empty($quiz_end) && isset($_SESSION['quiz_' . $quiz
    ->id()]['result_id'])) {

    // IMPORTANT: Because of a bug _quiz_get_answers always have to be called before quiz_end_scoring... :/
    $questions = _quiz_get_answers($quiz, $_SESSION['quiz_' . $quiz
      ->id()]['result_id']);
    $score = quiz_end_scoring($quiz, $_SESSION['quiz_' . $quiz
      ->id()]['result_id']);
    if ($quiz->feedback_time == QUIZ_FEEDBACK_NEVER) {
      $content['body']['#markup'] = theme('quiz_no_feedback');
    }
    else {

      // Get the results and summary text for this quiz.
      $summary = _quiz_get_summary_text($quiz, $score);
      $data = array(
        'quiz' => $quiz,
        'questions' => $questions,
        'score' => $score,
        'summary' => $summary,
        'rid' => $_SESSION['quiz_' . $quiz
          ->id()]['result_id'],
      );

      // Get the themed summary page.
      $content['body']['#markup'] = theme('quiz_take_summary', $data);
    }
    if ($score['is_evaluated']) {
      _quiz_maintain_results($quiz, $_SESSION['quiz_' . $quiz
        ->id()]['result_id']);
    }

    // Remove session variables, save $rid
    $session_data = $_SESSION['quiz_' . $quiz
      ->id()];
    unset($_SESSION['quiz_' . $quiz
      ->id()]);

    // NOTE: End actions might redirect the user somewhere. Code below this line might not get executed...
    quiz_end_actions($quiz, $score, $session_data);
  }
  return $content;
}

/**
 * Delete quiz responses for quizzes that haven't been finished.
 *
 * @param $quiz
 *   A quiz node where old in progress results shall be deleted.
 * @param $uid
 *   The userid of the user the old in progress results belong to.
 */
function _quiz_delete_old_in_progress($quiz, $uid) {
  $res = db_query('SELECT qnr.result_id FROM {quiz_node_results} qnr
          WHERE qnr.uid = :uid
          AND qnr.nid = :nid
          AND qnr.time_end = :time_end
          AND qnr.vid < :vid', array(
    ':uid' => $uid,
    ':nid' => $quiz
      ->id(),
    ':time_end' => 1,
    ':vid' => $quiz
      ->getRevisionId(),
  ));
  $rids = array();
  while ($rid = $res
    ->fetchField()) {
    $rids[] = $rid;
  }
  quiz_delete_results($rids);
}

/**
 * Delete quiz results.
 *
 * @param $rids
 *   Result ids for the results to be deleted.
 */
function quiz_delete_results($rids) {
  if (empty($rids)) {
    return;
  }
  $sql = 'SELECT result_id, question_nid, question_vid FROM {quiz_node_results_answers}
          WHERE result_id IN(:result_id)';
  $result = db_query($sql, array(
    ':result_id' => $rids,
  ));
  foreach ($result as $record) {
    quiz_question_delete_result($record->result_id, $record->question_nid, $record->question_vid);
  }
  db_delete('quiz_node_results_answers')
    ->condition('result_id', $rids, 'IN')
    ->execute();
  db_delete('quiz_node_results')
    ->condition('result_id', $rids, 'IN')
    ->execute();
}

/**
 * Returns the result ID for any current result set for the given quiz.
 *
 * @param $uid
 *   User ID
 * @param $nid
 *   Quiz node ID
 * @param $vid
 *   Quiz node version ID
 * @param $now
 *   Timestamp used to check whether the quiz is still open. Default: current
 *   time.
 *
 * @return
 *   If a quiz is still open and the user has not finished the quiz,
 *   return the result set ID so that the user can continue. If no quiz is in
 *   progress, this will return 0.
 */
function _quiz_active_result_id($uid, $nid, $vid, $now = NULL) {
  if (!isset($now)) {
    $now = REQUEST_TIME;
  }

  // Get any quiz that is open, for this user, and has not already
  // been completed.
  $rid = db_query('SELECT result_id FROM {quiz_node_results} qnr
          INNER JOIN {quiz_node_properties} qnp ON qnr.vid = qnp.vid
          WHERE (qnp.quiz_always = :quiz_always OR (:between BETWEEN qnp.quiz_open AND qnp.quiz_close))
          AND qnr.vid = :vid
          AND qnr.uid = :uid
          AND qnr.time_end = :time_end', array(
    ':quiz_always' => 1,
    ':between' => $now,
    ':vid' => $vid,
    ':uid' => $uid,
    ':time_end' => 0,
  ))
    ->fetchField();
  return (int) $rid;
}

/**
 * Actions to take place at the start of a quiz.
 *
 * This is called when the quiz node is viewed for the first time. It ensures
 * that the quiz can be taken at this time.
 *
 * @param $quiz
 *   The quiz node.
 * @param $rid
 *   The result ID
 *
 * @return
 *   Return quiz_node_results result_id, or FALSE if there is an error.
 */
function quiz_start_check($quiz, $rid) {
  $user = \Drupal::currentUser();
  $user_is_admin = $user
    ->hasPermission('edit any quiz content') || $user
    ->hasPermission('edit own quiz content') && $quiz
    ->getAuthorId() == $user
    ->id();

  // Make sure this is available.
  if ($quiz->quiz_always != 1) {

    // Compare current GMT time to the open and close dates (which should still
    // be in GMT time).
    $now = gmmktime();
    if ($now >= $quiz->quiz_close || $now < $quiz->quiz_open) {
      if ($user_is_admin) {
        drupal_set_message(t('You are marked as an administrator or owner for this quiz. While you can take this quiz, the open/close times prohibit other users from taking this quiz.'), 'status');
      }
      else {
        drupal_set_message(t('This @quiz is not currently available.', array(
          '@quiz' => QUIZ_NAME,
        )), 'status');

        // Can't take quiz.
        return FALSE;
      }
    }
  }

  // Check to see if this user is allowed to take the quiz again:
  if ($quiz->takes > 0) {
    $query = "SELECT COUNT(*) AS takes FROM {quiz_node_results} WHERE uid = %d AND nid = %d";
    $taken = db_query("SELECT COUNT(*) AS takes FROM {quiz_node_results} WHERE uid = :uid AND nid = :nid", array(
      ':uid' => $user
        ->id(),
      ':nid' => $quiz
        ->id(),
    ))
      ->fetchField();
    $allowed_times = format_plural($quiz->takes, '1 time', '@count times');
    $taken_times = format_plural($taken, '1 time', '@count times');

    // The user has already taken this quiz.
    if ($taken) {
      if ($user_is_admin) {
        drupal_set_message(t('You have taken this quiz already. You are marked as an owner or administrator for this quiz, so you can take this quiz as many times as you would like.'), 'status');
      }
      elseif ($taken >= $quiz->takes) {
        drupal_set_message(t('You have already taken this quiz @really. You may not take it again.', array(
          '@really' => $taken_times,
        )), 'error');
        return FALSE;
      }
      elseif ($quiz->show_attempt_stats) {
        drupal_set_message(t("You can only take this quiz @allowed. You have taken it @really.", array(
          '@allowed' => $allowed_times,
          '@really' => $taken_times,
        )), 'status');
      }
    }
  }

  // Check to see if the user is registered, and user alredy passed this quiz.
  if ($quiz->show_passed && $user
    ->id() && quiz_is_passed($user
    ->id(), $quiz
    ->id(), $quiz
    ->getRevisionId())) {
    drupal_set_message(t('You have already passed this @quiz.', array(
      '@quiz' => QUIZ_NAME,
    )), 'status');
  }

  // Call hook_quiz_begin().

  //TODO: Use \Drupal::moduleHandler()->invoke($module, $hook, $args = array()). Instead module_invoke_all().
  module_invoke_all('quiz_begin', $quiz, $rid);
  return TRUE;
}

/**
 * Check a user/quiz combo to see if the user passed the given quiz.
 *
 * This will return TRUE if the user has passed the quiz at least once, and
 * FALSE otherwise. Note that a FALSE may simply indicate that the user has not
 * taken the quiz.
 *
 * @param $uid
 *   The user ID.
 * @param $nid
 *   The node ID.
 * @param $vid
 *   The version ID.
 */
function quiz_is_passed($uid, $nid, $vid) {
  $passed = db_query('SELECT COUNT(result_id) AS passed_count FROM {quiz_node_results} qnrs
    INNER JOIN {quiz_node_properties} USING (vid, nid)
    WHERE qnrs.vid = :vid
      AND qnrs.nid = :nid
      AND qnrs.uid = :uid
      AND score >= pass_rate', array(
    ':vid' => $vid,
    ':nid' => $nid,
    ':uid' => $uid,
  ))
    ->fetchField();

  // Force into boolean context.
  return $passed !== FALSE && $passed > 0;
}

/**
 * Set the current session to jump to a specific question number
 *
 * @param int $question_num
 *  The question number we want to jump to
 */
function quiz_jump_to($question_num, $quiz, $rid) {
  $num_next = count($_SESSION['quiz_' . $quiz
    ->id()]['quiz_questions']);
  $num_previous = count($_SESSION['quiz_' . $quiz
    ->id()]['previous_quiz_questions']);
  if ($question_num > $num_next + $num_previous + 1) {

    // Trying to jump too far. We allow skipping all questions because of jQuery countdown...
    return;
  }
  if ($question_num <= $num_previous) {
    for ($i = 0; $i < $num_previous - $question_num + 1; $i++) {
      array_unshift($_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions'], array_pop($_SESSION['quiz_' . $quiz
        ->id()]['previous_quiz_questions']));
    }
  }
  elseif ($question_num > $num_previous + 1) {
    for ($i = 0; $i < $question_num - $num_previous - 1; $i++) {
      $_SESSION['quiz_' . $quiz
        ->id()]['previous_quiz_questions'][] = array_shift($_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions']);
    }
    _quiz_cleanup_after_jumping($quiz, $rid);
  }
}

/**
 * Clean up result data when the user jumps forward in a quiz
 *
 * When jumping in a quiz we might be skipping lots of questions. We need
 * to update the result data according to this...
 *
 * @param object $quiz
 *  The quiz node we want to clean up results for
 * @param int $rid
 *  Result id for the result we want to clean up
 */
function _quiz_cleanup_after_jumping($quiz, $rid) {
  foreach ($_SESSION['quiz_' . $quiz
    ->id()]['previous_quiz_questions'] as $previous_question) {

    // tried db_merge() instead of separate SELECT and INSERT query
    // but it marks all the questions as skipped.
    $condition = array(
      ':rid' => $rid,
      ':nid' => $previous_question['nid'],
      ':vid' => $previous_question['vid'],
    );
    $duplicate = db_query('SELECT * FROM {quiz_node_results_answers} WHERE result_id = :rid AND question_nid = :nid AND question_vid = :vid', $condition)
      ->fetchAssoc();
    if (empty($duplicate)) {
      db_insert('quiz_node_results_answers')
        ->fields(array(
        'result_id' => $rid,
        'question_nid' => $previous_question['nid'],
        'question_vid' => $previous_question['vid'],
        'is_skipped' => 1,
        'answer_timestamp' => REQUEST_TIME,
        'number' => $previous_question['number'],
        'tid' => isset($previous_question['tid']) ? $previous_question['tid'] : 0,
      ))
        ->execute();
    }
  }
}

//TODO: Need to add doc
function _quiz_take_quiz_init($quiz) {

  // Create question list.
  $questions = quiz_build_question_list($quiz);
  if ($questions === FALSE) {
    drupal_set_message(t('Not enough random questions were found. Please add more questions before trying to take this @quiz.', array(
      '@quiz' => QUIZ_NAME,
    )), 'error');
    return array(
      'body' => array(
        '#value' => ' ',
      ),
    );
  }
  if (count($questions) == 0) {
    drupal_set_message(t('No questions were found. Please !assign_questions before trying to take this @quiz.', array(
      '@quiz' => QUIZ_NAME,
      '!assign_questions' => l(t('assign questions'), 'node/' . arg(1) . '/questions'),
    )), 'error');
    return array(
      'body' => array(
        '#value' => t('Please assign questions...'),
      ),
    );
  }

  // Initialize session variables.
  $_SESSION['quiz_' . $quiz
    ->id()]['result_id'] = quiz_create_rid($quiz);
  $_SESSION['quiz_' . $quiz
    ->id()]['quiz_questions'] = $questions;
  $_SESSION['quiz_' . $quiz
    ->id()]['previous_quiz_questions'] = array();
  $_SESSION['quiz_' . $quiz
    ->id()]['question_number'] = 0;
  $_SESSION['quiz_' . $quiz
    ->id()]['question_start_time'] = REQUEST_TIME;
  $_SESSION['quiz_' . $quiz
    ->id()]['quiz_vid'] = $quiz
    ->getRevisionId();
  if ($quiz->time_limit > 0) {
    $_SESSION['quiz_' . $quiz
      ->id()]['question_duration'] = $quiz->time_limit;
  }
}

/**
 * Retrieves a list of questions (to be taken) for a given quiz.
 *
 * If the quiz has random questions this function only returns a random
 * selection of those questions. This function should be used to decide
 * what questions a quiz taker should answer.
 *
 * @param $quiz
 *   Quiz node.
 * @return
 *   Array of question node IDs.
 */
function quiz_build_question_list($quiz) {
  $questions = array();
  if ($quiz->randomization == 3) {
    return _quiz_build_categorized_question_list($quiz);
  }

  // Get required questions first.
  $query = db_query('SELECT child_nid as nid, child_vid as vid, max_score as relative_max_score
    FROM {quiz_node_relationship} qnr
    JOIN {node} n ON qnr.child_nid = n.nid
    JOIN {node_field_data} nd ON qnr.child_nid = nd.nid
    WHERE qnr.parent_vid = :parent_vid
    AND qnr.question_status = :question_status
    AND nd.status = 1
    ORDER BY weight', array(
    ':parent_vid' => $quiz
      ->getRevisionId(),
    ':question_status' => QUESTION_ALWAYS,
  ));
  while ($question_node = $query
    ->fetchAssoc()) {
    $questions[] = $question_node;
  }

  // Get random questions for the remainder.
  if ($quiz->number_of_random_questions > 0) {
    $random_questions = _quiz_get_random_questions($quiz);
    $questions = array_merge($questions, $random_questions);
    if ($quiz->number_of_random_questions > count($random_questions)) {

      // Unable to find enough requested random questions.
      return FALSE;
    }
  }

  // Shuffle questions if required.
  if ($quiz->randomization > 0) {
    shuffle($questions);
  }
  $count = 0;
  foreach ($questions as &$question) {
    $count++;
    $question['number'] = $count;
  }
  return $questions;
}

/**
 * Creates a unique id to be used when storing results for a quiz taker.
 *
 * @param $quiz
 *   The quiz node.
 * @return $rid
 *   The result id.
 */
function quiz_create_rid($quiz) {
  $rid = db_insert('quiz_node_results')
    ->fields(array(
    'nid' => $quiz
      ->id(),
    'vid' => $quiz
      ->getRevisionId(),
    'uid' => \Drupal::currentUser()
      ->id(),
    'time_start' => REQUEST_TIME,
  ))
    ->execute();
  if (!is_numeric($rid)) {
    form_set_error(t('There was a problem starting the @quiz. Please try again later.', array(
      '@quiz' => QUIZ_NAME,
    ), array(
      'langcode' => 'error',
    )), $form_state);
    return FALSE;
  }
  return $rid;
}

/**
 * Finds out the number of questions for the quiz.
 *
 * Good example of usage could be to calculate the % of score.
 *
 * @param $nid
 *   Quiz ID.
 * @return
 *   Returns the number of quiz questions.
 */
function quiz_get_number_of_questions($vid) {
  $always_count = _quiz_get_num_always_questions($vid);
  $rand_count = db_query('SELECT number_of_random_questions FROM {quiz_node_properties} WHERE vid = :vid', array(
    ':vid' => $vid,
  ))
    ->fetchField();
  return $always_count + (int) $rand_count;
}

/**
 * Create the view for a question the user is about to take.
 *
 * @param $question_node
 *   The question node that should be rendered.
 * @param $quiz_node
 *   The quiz node.
 * @return
 *   A string containing the body of the node.
 */
function quiz_take_question_view($question_node, $quiz_node) {
  $question_node->allow_skipping = $quiz_node->allow_skipping;
  $content = node_view($question_node, 'teaser');

  //TODO: Use \Drupal::moduleHandler()->invoke($module, $hook, $args = array()). Instead module_invoke_all().
  module_invoke_all('node_build_alter', $question_node, FALSE);

  //$question_node->body = drupal_render($question_node->content);
  return theme('quiz_single_question_node', array(
    'content' => $content,
  ));
}

/**
 * Helper function used when figuring out if a textfield or textarea is empty.
 *
 * Solves a problem with some wysiwyg editors inserting spaces and tags without content.
 *
 * @param $html
 *   The html to evaluate
 *
 * @return
 *   TRUE if the field is empty(can still be tags there) false otherwise.
 */
function _quiz_is_empty_html($html) {
  return drupal_strlen(trim(str_replace('&nbsp;', '', strip_tags($html, '<img><object><embed>')))) == 0;
}

/**
 * Implementation of hook_node_presave().
 */
function quiz_node_presave(EntityInterface $node) {
  if ($node
    ->getType() == 'quiz') {

    // Convert the action id to the actual id from the MD5 hash.
    // Why the actions module does this I do not know? Maybe to prevent
    // invalid values put into the options value="" field.
    if (!empty($node->aid) && ($aid = actions_function_lookup($node->aid))) {
      $node->aid = $aid;
    }
    if (\Drupal::config('quiz.settings')
      ->get('quiz_auto_revisioning')) {
      $node->revision = quiz_has_been_answered($node) ? 1 : 0;
    }
  }
  if (isset($node->is_quiz_question) && \Drupal::config('quiz.settings')
    ->get('quiz_auto_revisioning')) {
    $node->revision = quiz_question_has_been_answered($node) ? 1 : 0;
  }
}

/**
 * Implementation of hook_node_prepare_form().
 */
function quiz_node_prepare_form(NodeInterface $node, $form_display, $operation, array &$form_state) {
  if ($node
    ->getType() == 'quiz' && $node
    ->isNew()) {

    // If this is a new node we apply the user defaults for the quiz settings.
    $settings = _quiz_load_user_settings();
    if (!$settings) {
      if (arg(0) == 'node') {
        drupal_set_message(t('You are making your first @quiz. On this page you set the attributes, most of which you may tell the system to remember as defaults for the future. On the next screen you can add questions.', array(
          '@quiz' => QUIZ_NAME,
        )));
      }
      $settings = _quiz_load_user_settings(\Drupal::config('quiz.settings')
        ->get('quiz_def_uid'));
    }
    $settings += _quiz_get_node_defaults();
    foreach ($settings as $key => $value) {
      if (!isset($node->{$key})) {
        $node->{$key} = $value;
      }
    }
  }
  if (isset($node->is_quiz_question)) {
    if (\Drupal::config('quiz.settings')
      ->get('quiz_auto_revisioning')) {
      $node->revision = quiz_question_has_been_answered($node) ? 1 : 0;
    }
  }
}

/**
 * Get answer data for a specific result.
 *
 * @param $rid
 *   Result id.
 *
 * @return
 *   Array of answers.
 */
function _quiz_get_answers($quiz, $rid) {
  $questions = array();
  $ids = db_query("SELECT question_nid, question_vid, type, rs.max_score, qt.max_score as term_max_score\n                   FROM {quiz_node_results_answers} ra\n                   LEFT JOIN {node} n ON (ra.question_nid = n.nid)\n                   LEFT JOIN {quiz_node_results} r ON (ra.result_id = r.result_id)\n                   LEFT OUTER JOIN {quiz_node_relationship} rs ON (ra.question_vid = rs.child_vid) AND rs.parent_vid = r.vid\n                   LEFT OUTER JOIN {quiz_terms} qt ON (qt.vid = :vid AND qt.tid = ra.tid)\n                   WHERE ra.result_id = :rid\n                   ORDER BY ra.number, ra.answer_timestamp", array(
    ':vid' => $quiz
      ->getRevisionId(),
    ':rid' => $rid,
  ));
  foreach ($ids as $line) {

    // Questions picked from term id's won't be found in the quiz_node_relationship table
    if ($line->max_score === NULL) {
      if ($quiz->randomization == 2 && isset($quiz->tid) && $quiz->tid > 0) {
        $line->max_score = $quiz->max_score_for_random;
      }
      elseif ($quiz->randomization == 3) {
        $line->max_score = $line->term_max_score;
      }
    }
    $module = quiz_question_module_for_type($line->type);
    if (!$module) {
      continue;
    }

    // Invoke hook_get_report().
    $report = module_invoke($module, 'get_report', $line->question_nid, $line->question_vid, $rid);
    if (!$report) {
      continue;
    }
    $questions[$line->question_nid] = $report;

    // Add max score info to the question.
    if (!isset($questions[$line->question_nid]->score_weight)) {
      if ($questions[$line->question_nid]->max_score == 0) {
        $score_weight = 0;
      }
      else {
        $score_weight = $line->max_score / $questions[$line->question_nid]->max_score;
      }
      $questions[$line->question_nid]->qnr_max_score = $line->max_score;
      $questions[$line->question_nid]->score_weight = $score_weight;
    }
  }
  return $questions;
}

/**
 * Score a completed quiz.
 */
function quiz_end_scoring($quiz, $rid) {
  $user = \Drupal::currentUser();
  $score = quiz_calculate_score($quiz, $rid);
  if (!isset($score['percentage_score'])) {
    $score['percentage_score'] = 0;
  }
  db_update('quiz_node_results')
    ->fields(array(
    'is_evaluated' => $score['is_evaluated'],
    'time_end' => REQUEST_TIME,
    'score' => $score['percentage_score'],
  ))
    ->condition('result_id', $rid)
    ->execute();
  if ($user
    ->id()) {
    $score['passing'] = quiz_is_passed($user
      ->id(), $quiz
      ->id(), $quiz
      ->getRevisionId());
  }
  else {
    $score['passing'] = $score['percentage_score'] >= $quiz->pass_rate;
  }
  return $score;
}

/**
 * Calculates the score user received on quiz.
 *
 * @param $quiz
 *   The quiz node.
 * @param $rid
 *   Quiz result ID.
 *
 * @return array
 *   Contains three elements: question_count, num_correct and percentage_score.
 */
function quiz_calculate_score($quiz, $rid) {

  // 1. Fetch all questions and their max scores
  $questions = db_query('SELECT a.question_nid, a.question_vid, n.type, r.max_score
    FROM {quiz_node_results_answers} a
    LEFT JOIN {node} n ON (a.question_nid = n.nid)
    LEFT OUTER JOIN {quiz_node_relationship} r ON (r.child_vid = a.question_vid) AND r.parent_vid = :vid
    WHERE result_id = :rid', array(
    ':vid' => $quiz
      ->getRevisionId(),
    ':rid' => $rid,
  ));

  // 2. Callback into the modules and let them do the scoring. @todo after 4.0: Why isn't the scores already saved? They should be
  // Fetched from the db, not calculated....
  $scores = array();
  $count = 0;
  foreach ($questions as $question) {

    // Questions picked from term id's won't be found in the quiz_node_relationship table
    if ($question->max_score === NULL && isset($quiz->tid) && $quiz->tid > 0) {
      $question->max_score = $quiz->max_score_for_random;
    }

    // Invoke hook_quiz_question_score().
    // We don't use module_invoke() because (1) we don't necessarily want to wed
    // quiz type to module, and (2) this is more efficient (no NULL checks).
    $mod = quiz_question_module_for_type($question->type);
    if (!$mod) {
      continue;
    }
    $function = $mod . '_quiz_question_score';
    if (function_exists($function)) {
      $score = $function($quiz, $question->question_nid, $question->question_vid, $rid);

      // Allow for max score to be considered.
      $scores[] = $score;
    }
    else {
      drupal_set_message(t('A quiz question could not be scored: No scoring info is available'), 'error');
      $dummy_score = new stdClass();
      $dummy_score->possible = 0;
      $dummy_score->attained = 0;
      $scores[] = $dummy_score;
    }
    ++$count;
  }

  // 3. Sum the results.
  $possible_score = 0;
  $total_score = 0;
  $is_evaluated = TRUE;
  foreach ($scores as $score) {
    $possible_score += $score->possible;
    $total_score += $score->attained;
    if (isset($score->is_evaluated)) {

      // Flag the entire quiz if one question has not been evaluated.
      $is_evaluated &= $score->is_evaluated;
    }
  }

  // 4. Return the score.
  return array(
    'question_count' => $count,
    'possible_score' => $possible_score,
    'numeric_score' => $total_score,
    'percentage_score' => $possible_score == 0 ? 0 : round($total_score * 100 / $possible_score),
    'is_evaluated' => $is_evaluated,
  );
}

/**
 * Get the summary message for a completed quiz.
 *
 * Summary is determined by whether we are using the pass / fail options, how
 * the user did, and where the method is called from.
 *
 * @todo Need better feedback for when a user is viewing their quiz results
 *   from the results list (and possibily when revisiting a quiz they can't take
 *   again).
 *
 * @param $quiz
 *   The quiz node object.
 * @param $score
 *   The score information as returned by quiz_calculate_score().
 * @return
 *   Filtered summary text or null if we are not displaying any summary.
 */
function _quiz_get_summary_text($quiz, $score) {
  $summary = array();
  $admin = arg(0) == 'admin';
  if (!$admin) {
    if (!empty($score['result_option'])) {

      // Unscored quiz, return the proper result option.
      $summary['result'] = check_markup($score['result_option'], $quiz->body['und'][0]['format']);
    }
    else {
      $result_option = _quiz_pick_result_option($quiz
        ->id(), $quiz
        ->getRevisionId(), $score['percentage_score']);
      $summary['result'] = is_object($result_option) ? check_markup($result_option->option_summary, $result_option->option_summary_format) : '';
    }
  }

  // If we are using pass/fail, and they passed.
  if ($quiz->pass_rate > 0 && $score['percentage_score'] >= $quiz->pass_rate) {

    // If we are coming from the admin view page.
    if ($admin) {
      $summary['passfail'] = t('The user passed this quiz.');
    }
    elseif (\Drupal::config('quiz.settings')
      ->get('quiz_use_passfail') == 0) {

      // If there is only a single summary text, use this.
      if (trim($quiz->summary_default) != '') {
        $summary['passfail'] = check_markup($quiz->summary_default, $quiz->body['und'][0]['format']);
      }
    }
    elseif (trim($quiz->summary_pass) != '') {

      // If there is a pass summary text, use this.
      $summary['passfail'] = check_markup($quiz->summary_pass, $quiz->summary_pass_format);
    }
  }
  else {

    // If we are coming from the admin view page, only show a summary if we are
    // using pass/fail.
    if ($admin) {
      if ($quiz->pass_rate > 0) {
        $summary['passfail'] = t('The user failed this quiz.');
      }
      else {
        $summary['passfail'] = t('the user completed this quiz.');
      }
    }
    elseif (trim($quiz->summary_default) != '') {
      $summary['passfail'] = check_markup($quiz->summary_default, $quiz->summary_default_format);
    }
  }
  return $summary;
}

/**
 * Get summary text for a particular score from a set of result options.
 *
 * @param $qnid
 *   The quiz node id.
 * @param $qvid
 *   The quiz node revision id.
 * @param $score
 *   The user's final score.
 *
 * @return
 *   Summary text for the user's score.
 */
function _quiz_pick_result_option($qnid, $qvid, $score) {
  return db_query('SELECT option_summary, option_summary_format FROM {quiz_node_result_options}
      WHERE nid = :nid AND vid = :vid AND :option BETWEEN option_start AND option_end', array(
    ':nid' => $qnid,
    ':vid' => $qvid,
    ':option' => $score,
  ))
    ->fetch();
}

/**
 * Resume an in-progress quiz.
 *
 * This sets the user's session back to the state it was in when the quiz was
 * aborted.
 *
 * This function should only be called if the quiz needs resuming. Outside logic
 * needs to check that, though.
 *
 * @param $quiz
 *   The current quiz(node).
 * @param $uid
 *   The ID of the current user.
 * @param $rid
 *   The result ID found for the current quiz.
 */
function _quiz_resume_existing_quiz($quiz, $uid, $rid) {

  // Create question list.
  $questions = quiz_build_question_list($quiz);
  $already_answered = array();

  // Now we need to make sure to set previous questions to be correct.
  // This includes corrections for cases where questions were shuffled.
  $answered_questions = db_query('SELECT question_nid AS nid, question_vid AS vid, number
          FROM {quiz_node_results_answers}
          WHERE result_id = :rid
          ORDER BY number, answer_timestamp', array(
    ':rid' => $rid,
  ));

  //while ($answered = db_fetch_object($answered_questions)) {
  foreach ($answered_questions as $answered) {
    foreach ($questions as $question) {
      if ($question['vid'] == $answered->vid) {
        $already_answered[] = $answered->vid;

        // we found it, no need to continue the foreach loop
        continue 2;
      }
    }

    // This question was answered, but wasn't in $questions, so we need
    // to replace an unanswered question in $questions with it.
    if (!in_array($answered->vid, $already_answered)) {
      foreach ($questions as $key => $question) {
        if (!in_array($question['vid'], $already_answered) && isset($question['random']) && $question['random'] === TRUE) {
          $questions[$key]['vid'] = $answered->vid;
          $questions[$key]['nid'] = $answered->nid;
          $questions[$key]['rid'] = $rid;
          $already_answered[] = $answered->vid;
          continue 2;
        }
      }
    }
  }

  // Adding data to the session.
  reset($questions);
  $_SESSION['quiz_' . $quiz
    ->id()]['previous_quiz_questions'] = array();
  $_SESSION['quiz_' . $quiz
    ->id()]['quiz_questions'] = array();
  $next_number = count($already_answered) + 1;
  foreach ($questions as $question) {
    if (in_array($question['vid'], $already_answered)) {
      $question['rid'] = $rid;
      $_SESSION['quiz_' . $quiz
        ->id()]['previous_quiz_questions'][] = $question;
    }
    else {
      $question['number'] = $next_number;
      $next_number++;
      $_SESSION['quiz_' . $quiz
        ->id()]['quiz_questions'][] = $question;
    }
  }
  if (empty($_SESSION['quiz_' . $quiz
    ->id()]['quiz_questions']) && count($_SESSION['quiz_' . $quiz
    ->id()]['previous_quiz_questions']) == count($already_answered)) {
    $_SESSION['quiz_' . $quiz
      ->id()]['quiz_questions'][] = array_pop($_SESSION['quiz_' . $quiz
      ->id()]['previous_quiz_questions']);
  }
  $_SESSION['quiz_' . $quiz
    ->id()]['result_id'] = $rid;
  $_SESSION['quiz_' . $quiz
    ->id()]['question_number'] = count($_SESSION['quiz_' . $quiz
    ->id()]['previous_quiz_questions']);

  // Timed quizzes are likely to have expired by this point. But we let
  // the quiz_take_quiz handler handle that.
  $_SESSION['quiz_' . $quiz
    ->id()]['question_start_time'] = REQUEST_TIME;
  $_SESSION['quiz_' . $quiz
    ->id()]['question_duration'] = $quiz->time_limit;
  drupal_set_message(t('Resuming a previous quiz in-progress.'), 'status');
}

/**
 * Deletes results for a quiz according to the keep results setting
 *
 * @param $quiz
 *  The quiz node to be maintained
 * @param $rid
 *  The result id of the latest result for the current user
 * @return
 *  TRUE if results where deleted.
 */
function _quiz_maintain_results($quiz, $rid) {
  $user = \Drupal::currentUser();

  // Do not delete results for anonymous users
  if ($user
    ->id() == 0) {
    return;
  }
  switch ($quiz->keep_results) {
    case QUIZ_KEEP_ALL:
      return FALSE;
    case QUIZ_KEEP_BEST:
      $best_result_id = db_query('SELECT result_id FROM {quiz_node_results}
          WHERE nid = :nid AND uid = :uid AND is_evaluated = :is_evaluated
          ORDER BY score DESC', array(
        ':nid' => $quiz
          ->id(),
        ':uid' => $user
          ->id(),
        ':is_evaluated' => 1,
      ))
        ->fetchField();
      if (!$best_result_id) {
        return;
      }
      $res = db_query('SELECT result_id FROM {quiz_node_results}
          WHERE nid = :nid AND uid = :uid AND result_id != :best_rid AND is_evaluated = :is_evaluated', array(
        ':nid' => $quiz
          ->id(),
        ':uid' => $user
          ->id(),
        ':is_evaluated' => 1,
        ':best_rid' => $best_result_id,
      ));
      $rids = array();
      while ($rid2 = $res
        ->fetchField()) {
        $rids[] = $rid2;
      }
      quiz_delete_results($rids);
      return !empty($rids);
    case QUIZ_KEEP_LATEST:
      $res = db_query('SELECT result_id FROM {quiz_node_results}
              WHERE nid = :nid AND uid = :uid AND is_evaluated = :is_evaluated AND result_id != :result_id', array(
        ':nid' => $quiz
          ->id(),
        ':uid' => $user
          ->id(),
        ':is_evaluated' => 1,
        ':result_id' => $rid,
      ));
      $rids = array();
      while ($rid2 = $res
        ->fetchField()) {
        $rids[] = $rid2;
      }
      quiz_delete_results($rids);
      return !empty($rids);
  }
}

/**
 * Actions to take at the end of a quiz
 *
 * @param $quiz
 *  The quiz node
 * @param $rid
 *  Result id
 * @param $score
 *  Score as a number
 */
function quiz_end_actions($quiz, $score, $session_data) {

  // Call hook_quiz_finished().

  //TODO: Use \Drupal::moduleHandler()->invoke($module, $hook, $args = array()). Instead module_invoke_all().
  module_invoke_all('quiz_finished', $quiz, $score, $session_data);

  // Lets piggy back here to perform the quiz defined action since were done
  // with this quiz.
  // We will verify that there is an associated action with this quiz and then
  // perform that action.
  if (!empty($quiz->aid)) {

    // Some actions are reliant on objects and I am unsure which ones, for now I
    // have simply passed the actions_do() function an empty array. By passing
    // this function a single id then it will retrieve the callback, get the
    // parameters and perform that function (action) for you.
    actions_do($quiz->aid, $quiz, $score, $session_data);
  }
  return $score;
}

/**
 * Store a quiz question result.
 *
 * @param $quiz
 *  The quiz node
 * @param $result
 *  Object with data about the result for a question.
 * @param $options
 *  Array with options that affect the behavior of this function.
 *  ['set_msg'] - Sets a message if the last question was skipped.
 */
function quiz_store_question_result($quiz, $result, $options) {

  //drupal_set_message('<pre>'. print_r($result, 1) .'</pre>');
  if (isset($result->is_skipped) && $result->is_skipped == TRUE) {
    if ($options['set_msg']) {
      drupal_set_message(t('Last question skipped.'), 'status');
    }
    $result->is_correct = FALSE;
    $result->score = 0;
  }
  else {

    // Make sure this is set.
    $result->is_skipped = FALSE;
  }
  if (!isset($result->score)) {
    $result->score = $result->is_correct ? 1 : 0;
  }

  // Points are stored pre-scaled in the quiz_node_results_answers table. We get the scale.
  if ($quiz->randomization < 2) {
    $scale = db_query("SELECT (max_score / (\n                  SELECT max_score\n                  FROM {quiz_question_properties}\n                  WHERE nid = :nid AND vid = :vid\n                )) as scale\n                FROM {quiz_node_relationship}\n                WHERE parent_nid = :parent_nid\n                AND parent_vid = :parent_vid\n                AND child_nid = :child_nid\n                AND child_vid = :child_vid\n               ", array(
      ':nid' => $result->nid,
      ':vid' => $result->vid,
      ':parent_nid' => $quiz
        ->id(),
      ':parent_vid' => $quiz
        ->getRevisionId(),
      ':child_nid' => $result->nid,
      ':child_vid' => $result->vid,
    ))
      ->fetchField();
  }
  elseif ($quiz->randomization == 2) {
    $scale = db_query("SELECT (max_score_for_random / (\n                  SELECT max_score\n                  FROM {quiz_question_properties}\n                  WHERE nid = :nid AND vid = :vid\n                )) as scale\n                FROM {quiz_node_properties}\n                WHERE vid = :vid\n               ", array(
      ':nid' => $result->nid,
      ':vid' => $result->vid,
      ':vid' => $quiz
        ->getRevisionId(),
    ))
      ->fetchField();
  }
  elseif ($quiz->randomization == 3) {
    if (isset($options['question_data']['tid'])) {
      $result->tid = $options['question_data']['tid'];
    }
    $scale = db_query("SELECT (max_score / (\n                  SELECT max_score\n                  FROM {quiz_question_properties}\n                  WHERE nid = :nid AND vid = :vid\n                )) as scale\n                FROM {quiz_terms}\n                WHERE vid = :vid\n                AND tid = :tid\n               ", array(
      ':nid' => $result->nid,
      ':vid' => $result->vid,
      ':vid' => $quiz
        ->getRevisionId(),
      ':tid' => $result->tid,
    ))
      ->fetchField();
  }
  $points = round($result->score * $scale);

  // Insert result data, or update existing data.
  $rid_count = db_query("SELECT COUNT('result_id') AS count\n              FROM {quiz_node_results_answers}\n              WHERE question_nid = :question_nid\n              AND question_vid = :question_vid\n              AND result_id = :result_id", array(
    ':question_nid' => $result->nid,
    ':question_vid' => $result->vid,
    ':result_id' => $result->rid,
  ))
    ->fetchField();
  if ($rid_count > 0) {
    $update = db_update('quiz_node_results_answers')
      ->fields(array(
      'is_correct' => (int) $result->is_correct,
      'points_awarded' => $points,
      'answer_timestamp' => REQUEST_TIME,
      'is_skipped' => (int) $result->is_skipped,
      'is_doubtful' => (int) $result->is_doubtful,
      'tid' => $quiz->randomization == 3 && $result->tid ? $result->tid : 0,
    ));
    $args = array(
      $result->is_correct,
      $points,
      REQUEST_TIME,
      $result->is_skipped,
    );
    $update
      ->condition('question_nid', $result->nid);
    $update
      ->condition('question_vid', $result->vid);
    $update
      ->condition('result_id', $result->rid);
    $update
      ->execute();
  }
  else {
    $insert = db_insert('quiz_node_results_answers')
      ->fields(array(
      'question_nid',
      'question_vid',
      'result_id',
      'is_correct',
      'points_awarded',
      'answer_timestamp',
      'is_skipped',
      'is_doubtful',
      'number',
      'tid',
    ))
      ->values(array(
      'question_nid' => $result->nid,
      'question_vid' => $result->vid,
      'result_id' => $result->rid,
      'is_correct' => (int) $result->is_correct,
      'points_awarded' => $points,
      'answer_timestamp' => REQUEST_TIME,
      'is_skipped' => (int) $result->is_skipped,
      'is_doubtful' => (int) $result->is_doubtful,
      'number' => $options['question_data']['number'],
      'tid' => $quiz->randomization == 3 && $result->tid ? $result->tid : 0,
    ))
      ->execute();
  }
}

/**
 * @param $type
 *
 * @return string
 *   Name of module matching the question type, as given by quiz_question_info()
 *   hook.
 */
function quiz_question_module_for_type($type) {
  $types = _quiz_get_question_types();
  if (!isset($types[$type])) {
    drupal_set_message(t('The module for the questiontype %type is not enabled', array(
      '%type' => $type,
    )), 'warning');
    return FALSE;
  }
  return $types[$type]['module'];
}

/**
 * Determine who should have access to the My results tab.
 */
function _quiz_user_results_access($user_id) {
  global $user;
  return $user_id == $user->uid && \Drupal::currentUser()
    ->hasPermission('view own quiz results') || \Drupal::currentUser()
    ->hasPermission('view any quiz results');
}

/**
 * Modifies the format fieldset.
 *
 * Adds a class to all the format fieldsets and removes unwanted strings.
 * A javascript is added by the forms theme function to make sure all format
 * selectors follows the body field format selector.
 * Used when there are multiple format selectors on one page.
 *
 * Could be a deprecated function in d7
 *
 * @param $format
 *   The format fieldset.
 */
function _quiz_format_mod(&$format) {
  $format['#attributes']['class'] = array(
    'quiz-filter',
  );
  if (isset($format['format'])) {
    $format['format']['guidelines']['#value'] = ' ';
    foreach ($format as $key => $value) {
      if (is_numeric($key)) {
        $format[$key]['#value'] = ' ';
      }
    }
  }
}

/**
 * Get an array list of random questions for a quiz.
 *
 * @param $quiz
 *   The quiz node.
 *
 * @return
 *   Array of nid/vid combos for quiz questions.
 */
function _quiz_get_random_questions($quiz) {
  if (!is_object($quiz)) {
    drupal_set_message(t('The question pool cannot be generated.'), 'error');
    watchdog('quiz', '_quiz_get_random_questions was called incorrectly.', array(), WATCHDOG_ERROR);
    return FALSE;
  }
  $num_random = $quiz->number_of_random_questions;
  $tid = $quiz->tid;
  $questions = array();
  if ($num_random > 0) {
    if ($tid > 0) {
      $questions = _quiz_get_random_taxonomy_question_ids($tid, $num_random);
    }
    else {

      // Select random question from assigned pool.
      $result = db_query_range("SELECT child_nid as nid, child_vid as vid\n        FROM {quiz_node_relationship} qnr\n        JOIN {node} n on qnr.child_nid = n.nid\n        WHERE qnr.parent_vid = :parent_vid\n        AND qnr.parent_nid = :parent_nid\n        AND qnr.question_status = :question_status\n        AND n.status = 1\n        ORDER BY RAND()", 0, $quiz->number_of_random_questions, array(
        ':parent_vid' => $quiz
          ->getRevisionId(),
        ':parent_nid' => $quiz
          ->id(),
        ':question_status' => QUESTION_RANDOM,
      ));
      while ($question_node = $result
        ->fetchAssoc()) {
        $question_node['random'] = TRUE;
        $question_node['relative_max_score'] = $quiz->max_score_for_random;
        $questions[] = $question_node;
      }
    }
  }
  return $questions;
}

/**
 * Given a term ID, get all of the question nid/vids that have that ID.
 *
 * @param $tid
 *   Integer term ID.
 *
 * @return
 *   Array of nid/vid combos, like array(array('nid'=>1, 'vid'=>2)).
 */
function _quiz_get_random_taxonomy_question_ids($tid, $num_random) {
  if ($tid == 0) {
    return array();
  }

  // Select random questions by taxonomy.
  $term = entity_load('taxonomy_term', $tid);
  $tree = taxonomy_get_tree($term->vid, $term->tid);

  // Flatten the taxonomy tree, and just keep term id's.
  $term_ids[] = $term->tid;
  if (is_array($tree)) {
    foreach ($tree as $term) {
      $term_ids[] = $term->tid;
    }
  }
  $term_ids = implode(',', $term_ids);

  // Get all published questions with one of the allowed term ids.
  // TODO Please convert this statement to the D7 database API syntax.
  $result = db_query("SELECT n.nid, n.vid\n    FROM {node} n\n    INNER JOIN {taxonomy_index} tn USING (nid)\n    WHERE n.status = 1 AND tn.tid IN ({$term_ids})\n    AND n.type IN ('" . implode("','", array_keys(_quiz_get_question_types())) . "') ORDER BY RAND()");
  $questions = array();
  foreach ($result as $question_node) {
    $question_node['random'] = TRUE;
    $questions[] = $question_node;
  }
  return $questions;
}

/**
 * Format a number of seconds to a hh:mm:ss format.
 *
 * @param $time_in_sec
 *   Integers time in seconds.
 *
 * @return
 *   String time in min : sec format.
 */
function _quiz_format_duration($time_in_sec) {
  $hours = intval($time_in_sec / 3600);
  $min = intval(($time_in_sec - $hours * 3600) / 60);
  $sec = $time_in_sec % 60;
  if (strlen($min) == 1) {
    $min = '0' . $min;
  }
  if (strlen($sec) == 1) {
    $sec = '0' . $sec;
  }
  return "{$hours}:{$min}:{$sec}";
}

/**
 * This functions returns the default email subject and body format which will
 * be used at the end of quiz.
 */
function quiz_email_results_format($type, $target) {
  if ($type == 'subject') {
    if ($target == 'author') {
      return t('!title Results Notice from !sitename');
    }
    if ($target == 'taker') {
      return t('!title Results Notice from !sitename');
    }
  }
  if ($type == 'body') {
    if ($target == 'author') {
      return t('Dear !author') . "\n\n" . t('!taker attended the quiz !title on !date') . "\n" . t('Test Description : !desc') . "\n" . t('!taker got !correct out of !total points in !minutes minutes. Score given in percentage is !percentage') . "\n" . t('You can access the result here !url') . "\n";
    }
    if ($target == 'taker') {
      return t('Dear !taker') . "\n\n" . t('You attended the quiz !title on !date') . "\n" . t('Test Description : !desc') . "\n" . t('You got !correct out of !total points in !minutes minutes. Score given in percentage is !percentage') . "\n" . t('You can access the result here !url') . "\n";
    }
  }
}

/**
 * Get a list of all available quizzes.
 *
 * @param $uid
 *   An optional user ID. If supplied, only quizzes created by that user will be
 *   returned.
 *
 * @return
 *   A list of quizzes.
 */
function _quiz_get_quizzes($uid = 0) {
  $results = array();
  $args = array();
  $query = db_select('node', 'n')
    ->fields('n', array(
    'nid',
    'vid',
  ))
    ->fields('nd', array(
    'title',
    'uid',
    'created',
  ))
    ->fields('u', array(
    'name',
  ));
  $query
    ->leftJoin('node_field_data', 'nd', 'nd.nid = n.nid AND nd.vid = n.vid');
  $query
    ->leftJoin('users', 'u', 'u.uid = nd.uid');
  $query
    ->condition('n.type', 'quiz');
  if ($uid != 0) {
    $query
      ->condition('nd.uid', $uid);
  }
  $query
    ->orderBy('n.nid');
  $quizzes = $query
    ->execute();
  foreach ($quizzes as $quiz) {
    $results[$quiz->nid] = (array) $quiz;
  }
  return $results;
}

/**
 * Helper function used when validating plain text.
 *
 * @param $value
 *   The value to be validated.
 *
 * @return
 *   TRUE if plain text FALSE otherwise.
 */
function _quiz_is_plain($value) {
  return $value === check_plain($value);
}

/**
 * Update a score for a quiz.
 *
 * This updates the quiz node results table.
 *
 * It is used in cases where a quiz score is changed after the quiz has been
 * taken. For example, if a long answer question is scored later by a human,
 * then the quiz should be updated when that answer is scored.
 *
 * Important: The value stored in the table is the *percentage* score.
 *
 * @param $quiz
 *   The quiz node for the quiz that is being scored.
 * @param $rid
 *   The result ID to update.
 * @return
 *   The score as an integer representing percentage. E.g. 55 is 55%.
 */
function quiz_update_total_score($quiz, $rid) {
  $score = quiz_calculate_score($quiz, $rid);
  db_update('quiz_node_results')
    ->fields(array(
    'score' => $score['percentage_score'],
  ))
    ->condition('result_id', $rid)
    ->execute();
  if ($score['is_evaluated']) {

    // Call hook_quiz_scored().
    module_invoke_all('quiz_scored', $quiz, $score, $rid);
    _quiz_maintain_results($quiz, $rid);
    db_update('quiz_node_results')
      ->fields(array(
      'is_evaluated' => 1,
    ))
      ->condition('result_id', $rid)
      ->execute();
  }
  return $score['percentage_score'];
}

/**
 * Retrieves the quiz node from the menu router.
 *
 * @return
 *   Quiz node, if found, or FALSE if quiz node can't be retrieved from the menu
 *   router.
 */
function quiz_get_quiz_from_menu() {
  if ($to_return = menu_get_object('quiz_type_access', 4)) {
    return $to_return;
  }

  //TODO: FIX it. This seems to return NULL in feedback page.
  $node = menu_get_object();
  return is_object($node) && $node
    ->getType() == 'quiz' ? $node : FALSE;
}
function quiz_questions_browser_body_callback($form, $form_state) {
  return $form['question_list']['browser']['table']['body'];
}
function quiz_browser_body_callback($form, $form_state) {
  return $form['table']['body'];
}

Functions

Namesort descending Description
quiz_access_multi_or Helper function to check if the user has any of a given list of permissions.
quiz_access_my_result Helper function to determine if a user has access to view a specific quiz result.
quiz_access_my_results Helper function to determine if a user has access to view his quiz results
quiz_access_results Helper function to determine if a user has access to the different results pages.
quiz_access_to_score Helper function to determine if a user has access to score a quiz.
quiz_admin_paths Implements hook_admin_paths().
quiz_availability Find out if a quiz is available for taking or not
quiz_browser_body_callback
quiz_build_question_list Retrieves a list of questions (to be taken) for a given quiz.
quiz_calculate_score Calculates the score user received on quiz.
quiz_copy_questions Copies questions when a quiz is translated.
quiz_create_rid Creates a unique id to be used when storing results for a quiz taker.
quiz_cron @todo Please document this function.
quiz_delete_results Delete quiz results.
quiz_email_results_format This functions returns the default email subject and body format which will be used at the end of quiz.
quiz_end_actions Actions to take at the end of a quiz
quiz_end_scoring Score a completed quiz.
quiz_get_number_of_questions Finds out the number of questions for the quiz.
quiz_get_questions Retrieve list of published questions assigned to quiz.
quiz_get_quiz_from_menu Retrieves the quiz node from the menu router.
quiz_has_been_answered Finds out if a quiz has been answered or not.
quiz_help Implements hook_help().
quiz_is_passed Check a user/quiz combo to see if the user passed the given quiz.
quiz_jump_to Set the current session to jump to a specific question number
quiz_make_new Makes, saves and returns a new quiz node.
quiz_menu Implements hook_menu(). TODO: Migrate all menu url from D7 Quiz. Total 17 urls.
quiz_node_delete Implements hook_node_delete().
quiz_node_insert
quiz_node_load Implements hook_node_load().
quiz_node_map Map node properties to a question object.
quiz_node_prepare_form Implementation of hook_node_prepare_form().
quiz_node_presave Implementation of hook_node_presave().
quiz_node_view Implements hook_node_view().
quiz_permission Implements hook_permission().
quiz_questions_browser_body_callback
quiz_question_module_for_type
quiz_set_questions Sets the questions that are assigned to a quiz.
quiz_start_check Actions to take place at the start of a quiz.
quiz_start_quiz_button_form Helper function for hook_view().
quiz_store_question_result Store a quiz question result.
quiz_take_access Does the current user have access to take the quiz?
quiz_take_question_view Create the view for a question the user is about to take.
quiz_take_quiz Handles quiz taking.
quiz_theme Implements hook_theme().
quiz_type_access_load Load a quiz node, cache it and validate that it is indeed of type quiz.
quiz_update_max_score_properties Updates the max_score property on the specified quizzes
quiz_update_quiz_question_relationship Copies quiz-question relation entries in the quiz_node_relationship table from an old version of a quiz to a new.
quiz_update_total_score Update a score for a quiz.
quiz_user_cancel Implements hook_user_cancel().
quiz_views_api Implements hook_views_api().
template_preprocess_quiz_questions_browser_body Prepares variables for views exposed form templates.
_quiz_active_result_id Returns the result ID for any current result set for the given quiz.
_quiz_check_num_always If a quiz is saved with random categories we should make sure all questions are removed from the quiz
_quiz_check_num_random If a quiz is saved as not randomized we should make sure all random questions are converted to always.
_quiz_cleanup_after_jumping Clean up result data when the user jumps forward in a quiz
_quiz_common_presave_actions Common actions that need to be done before a quiz is inserted or updated
_quiz_delete_old_in_progress Delete quiz responses for quizzes that haven't been finished.
_quiz_delete_users_results Deletes all results associated with a given user.
_quiz_format_duration Format a number of seconds to a hh:mm:ss format.
_quiz_format_mod Modifies the format fieldset.
_quiz_form_prepare_date Takes a time element and prepares to send it to form_date().
_quiz_get_answers Get answer data for a specific result.
_quiz_get_feedback_options Get an array of feedback options.
_quiz_get_node_defaults Returns default values for all quiz settings.
_quiz_get_num_always_questions Get the number of compulsory questions for a quiz.
_quiz_get_question_types Retrieve list of question types.
_quiz_get_quizzes Get a list of all available quizzes.
_quiz_get_quiz_name Get the quiz name variable and set it as a constant so we don't have to keep calling it in every function.
_quiz_get_random_questions Get an array list of random questions for a quiz.
_quiz_get_random_taxonomy_question_ids Given a term ID, get all of the question nid/vids that have that ID.
_quiz_get_summary_text Get the summary message for a completed quiz.
_quiz_insert_resultoptions Insert call specific to result options.
_quiz_is_empty_html Helper function used when figuring out if a textfield or textarea is empty.
_quiz_is_int Helper function used when validating integers.
_quiz_is_last_question Check if this is the last question of the quiz
_quiz_is_plain Helper function used when validating plain text.
_quiz_is_taking_context Tells whether a question node is being viewed inside/outside of quiz.
_quiz_limit_year_options Limit the year options to the years 1970 - 2030 for form items of type date.
_quiz_load_user_settings Returns the users default settings.
_quiz_maintain_results Deletes results for a quiz according to the keep results setting
_quiz_pick_result_option Get summary text for a particular score from a set of result options.
_quiz_resume_existing_quiz Resume an in-progress quiz.
_quiz_save_user_settings Fetch settings from a node and save them as the users default settings.
_quiz_take_quiz_init
_quiz_update_resultoptions Modify result of option-specific updates.
_quiz_user_results_access Determine who should have access to the My results tab.

Constants