You are here

quiz.module in Quiz 6.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.
 */

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

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

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

/**
 * Implementation of hook_views_api().
 */
function quiz_views_api() {
  return array(
    'api' => 2,
    'path' => QUIZ_VIEWS_DIR,
  );
}

/**
 * Implementation of hook_perm().
 */
function quiz_perm() {
  return array(
    // Configure quiz:
    'administer quiz configuration',
    // Managing quizzes:
    'access quiz',
    'create quiz',
    'edit own quiz',
    'edit any quiz',
    'delete any quiz',
    'delete own quiz',
    // viewing results:
    'view any quiz results',
    'view own quiz results',
    'view results for own quiz',
    // deleting results:
    'delete any quiz results',
    'delete results for own quiz',
    // scoring:
    'score any quiz',
    'score own quiz',
    // Allow a quiz question to be viewed outside of a test.
    'view quiz question outside of a quiz',
    // Allow the user to see the correct answer, when viewed outside a quiz
    'view any quiz question correct response',
    // Allows users to pick a name for their questions. Otherwise this is auto
    // generated.
    'edit question titles',
    // Allow users to assign an action to be performed when a user has completed
    // a quiz:
    'assign any action to quiz events',
    // 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',
  );
}

/**
 * Implementation of hook_access().
 */
function quiz_access($op, $node, $account) {
  if (!user_access('access quiz', $account)) {

    // If users can't access, they'll get Nothing. Otherwise, we allow further
    // permission checking.
    return FALSE;
  }
  switch ($op) {
    case 'create':
      return user_access('create quiz', $account);
    case 'update':
      return user_access('edit any quiz', $account) || user_access('edit own quiz', $account) && $account->uid == $node->uid;
    case 'delete':
      return user_access('delete any quiz', $account) || user_access('delete own quiz', $account) && $account->uid == $node->uid;
  }
}

/**
 * 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;
  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)) {
    $sql = 'SELECT qnr.nid
            FROM {quiz_node_results} qnr
            WHERE result_id = %d';
    $res = db_result(db_query($sql, $rid));
    if ($res != $quiz->nid) {
      return FALSE;
    }
  }
  if (user_access('view any quiz results')) {
    return TRUE;
  }
  if (user_access('view results for own quiz') && $user->uid == $quiz->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 (user_access('view own quiz results') && !quiz_access_results($quiz)) {
    $answered = db_result(db_query_range('SELECT 1
       FROM {quiz_node_results}
       WHERE nid = %d AND uid = %d AND is_evaluated = 1', $quiz->nid, $user->uid, 0, 1));
    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) {
  global $user;
  if (!user_access('view own quiz results')) {
    return FALSE;
  }
  $time_end = db_result(db_query('SELECT time_end FROM {quiz_node_results} WHERE result_id = %d AND uid = %d', $rid, $user->uid));
  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 (user_access('score any quiz')) {
    return TRUE;
  }
  if (user_access('score own quiz') && $user->uid == $quiz_creator) {
    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 (user_access($perm)) {
      return TRUE;
    }
  }
}

/**
 * Implementation of hook_node_info().
 */
function quiz_node_info() {
  return array(
    'quiz' => array(
      'name' => t('@quiz', array(
        "@quiz" => QUIZ_NAME,
      )),
      'module' => 'quiz',
      'description' => 'Create interactive quizzes for site visitors',
    ),
  );
}

/**
 * Implementation of hook_init().
 *
 * Add quiz-specific styling.
 */
function quiz_init() {

  // @todo Probably don't want to add this to every page.
  drupal_add_css(drupal_get_path('module', 'quiz') . '/quiz.css', 'module', 'all');
}

/*
 * Implementation of hook_cron().
 */
function quiz_cron() {

  // Remove old quiz results that haven't been finished.
  $rm_time = variable_get('quiz_remove_partial_quiz_record', '0');
  if ($rm_time) {

    // $time = 0 for never.
    db_query('DELETE FROM {quiz_node_results} WHERE !time_end AND (%d - time_start) > %d', time(), $rm_time);
  }
}

/**
 * Implementation of hook_menu().
 */
function quiz_menu() {

  // Take quiz.
  $items['node/%node/take'] = array(
    'title' => 'Take',
    'page callback' => 'quiz_take',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'quiz_take_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_LOCAL_TASK,
  );

  // Admin pages.
  $items['admin/quiz'] = array(
    'title' => '@quiz management',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'View results, score answers, run reports and edit configurations.',
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array(
      'administer quiz configuration',
      'score any quiz',
      'score own quiz',
      'view any quiz results',
      'view results for own quiz',
    ),
    'access callback' => 'quiz_access_multi_or',
    'type' => MENU_NORMAL_ITEM,
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );
  $items['admin/quiz/settings'] = array(
    'title' => '@quiz settings',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'Change settings for the all Quiz project modules.',
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array(
      'administer quiz configuration',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );
  $items['admin/quiz/settings/config'] = array(
    'title' => '@quiz configuration',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'Configure the Quiz module.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quiz_admin_settings',
    ),
    'access arguments' => array(
      'administer quiz configuration',
    ),
    'type' => MENU_NORMAL_ITEM,
    // optional
    'file' => 'quiz.admin.inc',
  );
  $items['admin/quiz/settings/quiz_form'] = array(
    'title' => '@quiz form configuration',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'Configure default values for the quiz creation form.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quiz_admin_node_form',
    ),
    'access arguments' => array(
      'administer quiz configuration',
    ),
    'type' => MENU_NORMAL_ITEM,
    // optional
    'file' => 'quiz.admin.inc',
  );
  $items['admin/quiz/reports'] = array(
    'title' => 'Quiz reports and scoring',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'View reports and score answers.',
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array(
      'view any quiz results',
      'view results for own quiz',
    ),
    'access callback' => 'quiz_access_multi_or',
    'type' => MENU_NORMAL_ITEM,
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );
  $items['admin/quiz/reports/results'] = array(
    'title' => '@quiz results',
    'title arguments' => array(
      '@quiz' => QUIZ_NAME,
    ),
    'description' => 'View results.',
    'page callback' => 'quiz_admin_quizzes',
    'access arguments' => array(
      'view any quiz results',
      'view results for own quiz',
    ),
    'access callback' => 'quiz_access_multi_or',
    'type' => MENU_NORMAL_ITEM,
    'file' => 'quiz.admin.inc',
  );
  $items['node/%node/results'] = array(
    'title' => 'Results',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quiz_results_manage_results_form',
      1,
    ),
    'access arguments' => array(
      1,
    ),
    'access callback' => 'quiz_access_results',
    'type' => MENU_LOCAL_TASK,
    'file' => 'quiz.admin.inc',
    'weight' => 3,
  );
  $items['node/%node/myresults'] = array(
    'title' => 'My results',
    'page callback' => 'quiz_my_results',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'quiz_access_my_results',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'quiz.pages.inc',
    'weight' => 3,
  );
  $items['node/%node/myresults/%'] = array(
    'title' => 'User results',
    'page callback' => 'quiz_user_results',
    'page arguments' => array(
      3,
    ),
    'access arguments' => array(
      3,
    ),
    'access callback' => 'quiz_access_my_result',
    'type' => MENU_CALLBACK,
    'file' => 'quiz.pages.inc',
  );
  $items['node/%node/results/%'] = array(
    'title' => 'Results',
    'page callback' => 'quiz_admin_results',
    'page arguments' => array(
      1,
      3,
    ),
    'type' => MENU_CALLBACK,
    // , MENU_SUGGESTED_ITEM, MENU_LOCAL_TASK, MENU_DEFAULT_LOCAL_TASK
    'file' => 'quiz.admin.inc',
    'access arguments' => array(
      1,
      3,
    ),
    'access callback' => 'quiz_access_results',
  );

  // Add questions to quiz.
  $items['node/%node/questions'] = array(
    'title' => 'Manage questions',
    'page callback' => 'quiz_questions',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'quiz_type_confirm',
    'access arguments' => array(
      1,
      'update',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'quiz.admin.inc',
    'weight' => 2,
  );

  // User pages.
  $items['user/%/myresults'] = array(
    'title' => 'My results',
    'page callback' => 'quiz_get_user_results',
    'page arguments' => array(
      1,
    ),
    'access callback' => '_quiz_user_results_access',
    'access arguments' => array(
      1,
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'quiz.pages.inc',
  );
  $items['user/quiz/%/userresults'] = array(
    'title' => 'User results',
    'page callback' => 'quiz_user_results',
    'page arguments' => array(
      2,
    ),
    'access arguments' => array(
      2,
    ),
    'access callback' => 'quiz_access_my_result',
    'type' => MENU_CALLBACK,
    'file' => 'quiz.pages.inc',
  );
  $items['node/%node/questions/browser_ahah'] = array(
    //TODO: Add node access instead of user access...
    'page callback' => 'quiz_browser_ahah',
    'type' => MENU_CALLBACK,
    'access arguments' => array(
      'create quiz',
    ),
    'file' => 'quiz.admin.inc',
  );
  $items['node/%node/questions/term_ahah'] = array(
    //TODO: Add node access instead of user access...
    'page callback' => 'quiz_categorized_term_ahah',
    'type' => MENU_CALLBACK,
    'access arguments' => array(
      'create quiz',
    ),
    'file' => 'quiz.admin.inc',
  );
  $items['node/%node/results/browser_ahah'] = array(
    'page callback' => 'quiz_results_mr_browser_ahah',
    'type' => MENU_CALLBACK,
    'access callback' => 'quiz_access_results',
    'access arguments' => array(
      1,
    ),
    'file' => 'quiz.admin.inc',
  );
  return $items;
}

// Temporary fix, ref: http://drupal.org/node/777408
// TODO: Remove this if and when autoload adds a fix
function quiz_enable() {
  db_query("UPDATE {system} SET weight = -282828 WHERE name = 'autoload' AND type = 'module'");
}

/**
 * Implementation of hook_theme().
 */
function quiz_theme() {
  return array(
    'quiz_view_stats' => array(
      'arguments' => array(
        'node' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_categorized_form' => array(
      'arguments' => array(
        'form' => array(),
      ),
      'file' => 'quiz.admin.inc',
    ),
    'quiz_get_user_results' => array(
      'arguments' => array(
        'results' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_take_summary' => array(
      'arguments' => array(
        'quiz' => NULL,
        'questions' => NULL,
        'score' => 0,
        'summary' => '',
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_admin_summary' => array(
      'arguments' => array(
        'quiz' => NULL,
        'questions' => NULL,
        'score' => NULL,
        'summary' => NULL,
      ),
      'file' => 'quiz.admin.inc',
    ),
    'quiz_user_summary' => array(
      'arguments' => array(
        'quiz' => NULL,
        'questions' => NULL,
        'score' => NULL,
        'summary' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_progress' => array(
      'arguments' => array(
        'question_number' => NULL,
        'num_of_question' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_no_feedback' => array(
      'file' => 'quiz.pages.inc',
      'arguments' => array(),
    ),
    'quiz_admin_quizzes' => array(
      'file' => 'quiz.admin.inc',
      'arguments' => array(
        'results' => NULL,
      ),
    ),
    'quiz_single_question_node' => array(
      'file' => 'quiz.pages.inc',
      'arguments' => array(
        'question_node' => NULL,
      ),
    ),
    'question_selection_table' => array(
      'file' => 'quiz.admin.inc',
      'arguments' => array(
        'form' => array(),
      ),
    ),
    'quiz_score_correct' => array(
      'file' => 'quiz.pages.inc',
      'arguments' => array(),
    ),
    'quiz_score_incorrect' => array(
      'file' => 'quiz.pages.inc',
      'arguments' => array(),
    ),
    'quiz_question_browser' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'path' => drupal_get_path('module', 'quiz') . '/theme',
      'template' => 'quiz-question-browser',
    ),
    'quiz_results_browser' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'path' => drupal_get_path('module', 'quiz') . '/theme',
      'template' => 'quiz-results-browser',
    ),
    'quiz_report_form' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'path' => drupal_get_path('module', 'quiz') . '/theme',
      'template' => 'quiz-report-form',
    ),
    'quiz_node_form' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'file' => 'quiz.admin.inc',
    ),
    'quiz_jumper' => array(
      'arguments' => array(
        'current' => 0,
        'num_questions' => 0,
      ),
      'file' => 'quiz.admin.inc',
    ),
    'quiz_my_results_for_quiz' => array(
      'arguments' => array(
        'rows' => array(),
      ),
      'file' => 'quiz.admin.inc',
    ),
  );
}

/**
 * Implementation of hook_form_alter().
 *
 * Override settings in some existing forms. For example, we remove the
 * preview button on a quiz.
 */
function quiz_form_alter(&$form, $form_state, $form_id) {

  // Remove revision fieldset if user don't have access to revise quiz manually.
  if (isset($form['#quiz_check_revision_access'])) {
    if (!user_access('manual quiz revisioning') || variable_get('quiz_auto_revisioning', 1)) {
      $form['revision_information']['revision']['#type'] = 'value';
      $form['revision_information']['revision']['#value'] = $form['revision_information']['revision']['#default_value'];
      $form['revision_information']['log']['#type'] = 'value';
      $form['revision_information']['log']['#value'] = $form['revision_information']['log']['#default_value'];
      unset($form['revision_information']['#type'], $form['revision_information']['#title'], $form['revision_information']['#description']);
    }
    unset($form['buttons']['preview'], $form['buttons']['preview_changes']);
    $form['buttons']['submit']['#access'] = TRUE;

    // Quiz questions might want to add a cancel button.
    if (isset($form['#cancel_button'])) {
      $form['buttons']['cancel'] = array(
        '#type' => 'markup',
        '#value' => l(t('Cancel'), $form['#redirect']),
        '#weight' => 6,
      );
    }
    $form['buttons']['#weight'] = 50;
  }
}

/**
 * Implementation of hook_insert().
 */
function quiz_insert($node) {
  _quiz_save_user_settings($node);

  // Copy all the questions belonging to the quiz if this is a new translation.
  if ($node->is_new && isset($node->translation_source)) {
    quiz_copy_questions($node);
  }
  _quiz_common_presave_actions($node);
  $tid = isset($node->tid) ? $node->tid : 0;
  if (!isset($node->has_userpoints)) {
    $node->has_userpoints = 0;
  }
  $sql = "INSERT INTO {quiz_node_properties}\n    (vid, nid, aid, number_of_random_questions, randomization,\n    backwards_navigation, repeat_until_correct, quiz_open, quiz_close, takes,\n    show_attempt_stats, keep_results, time_limit, pass_rate, summary_pass,\n    summary_default, quiz_always, feedback_time, display_feedback, tid,\n    has_userpoints, allow_skipping, allow_resume, allow_jumping)\n    VALUES(%d, %d, '%s', %d, %d,\n    %d, %d, %d, %d, %d,\n    %d, %d, %d, %d, '%s',\n    '%s', %d, %d, %d, %d,\n    %d, %d, %d, %d)";

  // 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_query($sql, $node->vid, $node->nid, $node->aid, $node->number_of_random_questions, $node->randomization, $node->backwards_navigation, $node->repeat_until_correct, $node->quiz_open, $node->quiz_close, $node->takes, $node->show_attempt_stats, $node->keep_results, $node->time_limit, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always, $node->feedback_time, $node->display_feedback, $tid, isset($node->has_userpoints) ? $node->has_userpoints : 0, $node->allow_skipping, $node->allow_resume, $node->allow_jumping);
  _quiz_insert_resultoptions($node);
  if (!isset($node->auto_created) && $node->revision == 0) {
    drupal_set_message(t('You just created a new quiz. Now you have to add questions to it. This page is for adding and managing questions. Here you can create new questions or add some of your already created questions. If you want to change the quiz settings, you can use the "edit" tab.'));
    $_REQUEST['destination'] = url("node/{$node->nid}/questions");
  }

  // If the quiz don't have any questions and have been created manually jump to
  // the manage questions tab.
  if (!isset($node->auto_created)) {
    _quiz_redirect_if_empty($node);
  }
}

/**
 * Implementation of hook_update().
 */
function quiz_update($node) {
  _quiz_save_user_settings($node);

  // Quiz node vid (revision) was updated.
  if ($node->revision) {

    // Insert a new row in the quiz_node_properties table.
    $old_auto = isset($node->auto_created);
    $node->auto_created = TRUE;
    quiz_insert($node);
    if (!$old_auto) {
      unset($node->auto_created);
    }

    // Create new quiz-question relation entries in the quiz_node_relationship
    // table.
    quiz_update_quiz_question_relationship($node->old_vid, $node->vid, $node->nid);
  }
  else {

    // Update an existing row in the quiz_node_properties table.
    _quiz_common_presave_actions($node);
    $sql = "UPDATE {quiz_node_properties}\n      SET vid = %d,\n        aid='%s',\n        randomization = %d,\n        backwards_navigation = %d,\n        repeat_until_correct = %d,\n        quiz_open = %d,\n        quiz_close = %d,\n        takes = %d,\n        show_attempt_stats = %d,\n        keep_results = %d,\n        time_limit = '%d',\n        pass_rate = %d,\n        summary_pass = '%s',\n        summary_default = '%s',\n        quiz_always = %d,\n        feedback_time = %d,\n        display_feedback = %d,\n        number_of_random_questions = %d,\n        has_userpoints = %d,\n        allow_skipping = %d,\n        allow_resume = %d,\n        allow_jumping = %d\n      WHERE vid = %d\n        AND nid = %d";
    $resource = db_query($sql, $node->vid, $node->aid, $node->randomization, $node->backwards_navigation, $node->repeat_until_correct, $node->quiz_open, $node->quiz_close, $node->takes, $node->show_attempt_stats, $node->keep_results, $node->time_limit, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always, $node->feedback_time, $node->display_feedback, $node->number_of_random_questions, isset($node->has_userpoints) ? $node->has_userpoints : 0, $node->allow_skipping, $node->allow_resume, $node->allow_jumping, $node->vid, $node->nid);
    _quiz_update_resultoptions($node);
  }
  _quiz_check_num_random($node);
  _quiz_check_num_always($node);
  quiz_update_max_score_properties(array(
    $node->vid,
  ));

  // If the node has no questions redirect to the manage questions tab
  _quiz_redirect_if_empty($node);
}

/**
 * Implementation of hook_content_extra_fields(cck)
 */
function quiz_content_extra_fields($type_name) {
  $extra = array();
  if ($type_name == 'quiz') {
    $extra['taking'] = array(
      'label' => t('Taking options'),
      'description' => t('Fieldset for customizing how a quiz is taken'),
      'weight' => 0,
    );
    $extra['quiz_availability'] = array(
      'label' => t('Availability options'),
      'description' => t('Fieldset for customizing when a quiz is available'),
      'weight' => 0,
    );
    $extra['summaryoptions'] = array(
      'label' => t('Summary options'),
      'description' => t('Fieldset for customizing summaries in the quiz reports'),
      'weight' => 0,
    );
    $extra['resultoptions'] = array(
      'label' => t('Result options'),
      'description' => t('Fieldset for customizing result comments in quiz reports'),
      'weight' => 0,
    );
    $extra['remember_settings'] = array(
      'label' => t('Remember settings'),
      'description' => t('Checkbox for remembering quiz settings'),
      'weight' => 0,
    );
  }
  return $extra;
}
function quiz_title_callback($quiz_node) {
  return t('Results for "@quiz_title"', array(
    '@quiz_title' => $quiz_node->title,
  ));
}

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

/**
 * Implementation of hook_delete().
 */
function quiz_delete($node) {
  $sql = 'SELECT result_id
          FROM {quiz_node_results}
          WHERE nid = %d';
  $res = db_query($sql, $node->nid);
  $rids = array();
  while ($rids[] = db_result($res)) {
  }
  quiz_delete_results($rids);
  db_query('DELETE FROM {quiz_node_properties} WHERE nid = %d', $node->nid);
  db_query('DELETE FROM {quiz_node_relationship} WHERE parent_nid = %d', $node->nid);
  db_query('DELETE FROM {quiz_node_results} WHERE nid = %d', $node->nid);
  db_query('DELETE FROM {quiz_node_result_options} WHERE nid = %d', $node->nid);
}

/**
 * Returns default values for all quiz settings.
 *
 * @return
 *   Array of default values.
 */
function _quiz_get_node_defaults() {
  return array(
    'aid' => NULL,
    'number_of_random_questions' => 0,
    'max_score_for_random' => 1,
    'pass_rate' => 75,
    'summary_pass' => '',
    'summary_default' => '',
    '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,
  );
}

/**
 * Implementation of hook_load().
 */
function quiz_load($node) {

  // Fetching defaults.
  $default_additions = _quiz_get_node_defaults();
  $fields = implode(', ', array_keys($default_additions));

  // Fetching node settings.
  $sql = 'SELECT %s FROM {quiz_node_properties} WHERE vid = %d AND nid = %d';
  $fetched_additions = db_fetch_array(db_query($sql, $fields, $node->vid, $node->nid));
  $additions = $fetched_additions ? (object) ($fetched_additions += $default_additions) : NULL;

  // Fetching result options.
  $result_options = db_query('SELECT * FROM {quiz_node_result_options} WHERE nid = %d AND vid= %d', $node->nid, $node->vid);
  while ($option = db_fetch_array($result_options)) {
    $additions->resultoptions[$option['option_id']] = $option;
  }
  return $additions;
}

/**
 * Implementation of hook_view().
 */
function quiz_view($node, $teaser = FALSE, $page = FALSE) {
  drupal_alter('quiz_view', $node, $teaser, $page);
  $node = node_prepare($node, $teaser);

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

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

/**
 * 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_state, $node) {
  $form = array();
  $form['#action'] = url("node/{$node->nid}/take");
  $form['button'] = array(
    '#type' => 'button',
    '#value' => t('Start quiz'),
  );
  return $form;
}

/**
 * Primary quiz-taking view on 'Take' tab.
 */
function quiz_take($node) {
  drupal_alter('quiz_take', $node);
  if (isset($node->rendered_content)) {
    return $node->rendered_content;
  }
  $to_be_rendered = quiz_take_quiz($node);
  return drupal_render($to_be_rendered);
}

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

/**
 * Implementation of hook_form().
 *
 * This is an admin form used to build a new quiz. It is called as part of the
 * node edit form.
 */
function quiz_form(&$node) {
  $form = array();

  // If this is a new node we try to load the users settings.
  if (!isset($node->nid)) {
    $settings_loaded = _quiz_load_user_settings($node);
    if (!$settings_loaded) {
      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,
        )));
      }

      // The user had no settins stored. We load the settings for the default
      // user.
      $node->def_uid = variable_get('quiz_def_uid', 1);
      _quiz_load_user_settings($node);
    }
  }

  // We tell quiz_form_alter to check for the manual revisioning permission.
  $form['#quiz_check_revision_access'] = TRUE;
  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => t('Title'),
    '#default_value' => $node->title,
    '#description' => t('The name of the @quiz.', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#required' => TRUE,
  );
  $form['body_field']['body'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => $node->body,
    '#description' => t('A description of what the @quiz entails', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#required' => FALSE,
  );
  $form['body_field']['format'] = filter_form($node->format);

  // The body field format controls all the format selectors for this form.
  _quiz_format_mod($form['body_field']['format']);
  $form['taking'] = array(
    '#type' => 'fieldset',
    '#title' => t('Taking options'),
    '#collapsed' => isset($settings_loaded) ? $settings_loaded : FALSE,
    '#collapsible' => TRUE,
    '#attributes' => array(
      'id' => 'taking-fieldset',
    ),
  );
  $form['taking']['allow_resume'] = array(
    '#type' => 'checkbox',
    '#title' => t('Allow Resume'),
    '#default_value' => $node->allow_resume,
    '#description' => t('Whether to allow users to leave the @quiz incomplete and then resume it from where they left off.', array(
      '@quiz' => QUIZ_NAME,
    )),
  );
  $form['taking']['allow_skipping'] = array(
    '#type' => 'checkbox',
    '#title' => t('Allow Skipping questions'),
    '#default_value' => $node->allow_skipping,
    '#description' => t('Whether to allow users to skip questions in the @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
  );
  $form['taking']['allow_jumping'] = array(
    '#type' => 'checkbox',
    '#title' => t('Allow jumping'),
    '#default_value' => $node->allow_jumping,
    '#description' => t('Whether to allow users to jump between questions using a menu in the @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
  );
  $form['taking']['backwards_navigation'] = array(
    '#type' => 'checkbox',
    '#title' => t('Backwards navigation'),
    '#default_value' => $node->backwards_navigation,
    '#description' => t('Whether to allow user to go back and revisit their answers'),
  );
  $form['taking']['repeat_until_correct'] = array(
    '#type' => 'checkbox',
    '#title' => t('Repeat until correct'),
    '#default_value' => $node->repeat_until_correct,
    '#description' => t('Require the user to re-try the question until they answer it correctly.'),
  );
  $form['taking']['randomization'] = array(
    '#type' => 'radios',
    '#title' => t('Randomize questions'),
    '#options' => array(
      t('No randomization'),
      t('Random order'),
      t('Random questions'),
      t('Categorized random questions'),
    ),
    '#description' => t('The difference between "random order" and "random questions" is that with "random questions" questions are drawn randomly from a pool of questions. With "random order" the quiz will always consist of the same questions. With "Categorized random questions" you can choose several terms questions should be drawn from, and you can also choose how many questions that should be drawn from each, and max score for each term.'),
    '#default_value' => $node->randomization,
  );
  $form['taking']['feedback'] = array(
    '#type' => 'fieldset',
    '#title' => t('Feedback'),
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
  );
  $form['taking']['feedback']['feedback_time'] = array(
    '#title' => t('Feedback Time'),
    '#type' => 'radios',
    '#default_value' => $node->feedback_time,
    '#options' => _quiz_get_feedback_options(),
    '#description' => t('Indicates at what point feedback for each question will be given to the user'),
  );
  $form['taking']['feedback']['display_feedback'] = array(
    '#title' => t('Display solution'),
    '#type' => 'checkbox',
    '#default_value' => $node->display_feedback,
    '#description' => t('Display the users answers and the correct answers for all questions along with the score for each question.'),
  );

  // Added the action as a dropdown for selection with specific quizzes. This
  // allows you to choose a defined action from the actions module for use when
  // a user completes the quiz.
  if (user_access('assign any action to quiz events')) {
    $form['taking']['aid'] = array(
      '#title' => t('Assign Action'),
      '#description' => t('Select an action to be preformed after a user has completed this @quiz.', array(
        '@quiz' => QUIZ_NAME,
      )),
      '#type' => 'select',
      // @todo An idea here would be to add a system conf variable into the
      //   quiz_action_options() function that could filter the type of actions
      //   you could display on your quizzes.  For Example: you create a custom
      //   module that defines some actions that you only want a user to choose
      //   when creating a quiz and selecting an action from the dropdown.  You
      //   setup your actions with type 'quiz' and then add in that variable
      //   into the function and it will automatically filter and show only
      //   thosespecific actions.  @note:  In doing this you lose your default
      //   "Choose an Action" option.  Review actions and the
      //   quiz_action_options() function for further explaination.
      '#options' => quiz_action_options(variable_get('quiz_action_type', 'all')),
      '#default_value' => MD5($node->aid),
    );
  }
  $options = array(
    t('Unlimited'),
  );
  for ($i = 1; $i < 10; $i++) {
    $options[$i] = $i;
  }
  $form['taking']['multiple_takes'] = array(
    '#type' => 'fieldset',
    '#title' => t('Multiple takes'),
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
    '#attributes' => array(
      'id' => 'multiple-takes-fieldset',
    ),
  );
  $form['taking']['multiple_takes']['takes'] = array(
    '#type' => 'select',
    '#title' => t('Allowed number of attempts'),
    '#default_value' => $node->takes,
    '#options' => $options,
    '#description' => t('The number of times a user is allowed to take the @quiz. <strong>Anonymous users are only allowed to take quizzes that allow an unlimited number of attempts.</strong>', array(
      '@quiz' => QUIZ_NAME,
    )),
  );
  $form['taking']['multiple_takes']['show_attempt_stats'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display allowed number of attempts'),
    '#default_value' => $node->show_attempt_stats,
    '#description' => t('Display the allowed number of attempts on the starting page for this quiz.'),
  );
  if (user_access('delete any quiz results') || user_access('delete results for own quiz')) {
    $form['taking']['multiple_takes']['keep_results'] = array(
      '#type' => 'radios',
      '#title' => t('These results should be stored for each user'),
      '#options' => array(
        t('The best'),
        t('The newest'),
        t('All'),
      ),
      '#default_value' => $node->keep_results,
    );
  }
  else {
    $form['taking']['multiple_takes']['keep_results'] = array(
      '#type' => 'value',
      '#value' => $node->keep_results,
    );
  }
  if (function_exists('jquery_countdown_add') && variable_get('quiz_has_timer', 0)) {
    $form['taking']['addons'] = array(
      '#type' => 'fieldset',
      '#title' => t('Quiz Addons Properties'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
    );
    $form['taking']['addons']['time_limit'] = array(
      '#type' => 'textfield',
      '#title' => t(' Time Limit'),
      '#default_value' => isset($node->time_limit) ? $node->time_limit : 0,
      '#description' => t('Set the maximum allowed time in seconds for this @quiz. Use 0 for no limit.', array(
        '@quiz' => QUIZ_NAME,
      )),
    );
  }
  else {
    $form['taking']['addons']['time_limit'] = array(
      '#type' => 'value',
      '#value' => 0,
    );
  }
  if (module_exists('userpoints') && variable_get('quiz_has_userpoints', 0)) {
    $form['has_userpoints'] = array(
      '#type' => 'checkbox',
      '#default_value' => isset($node->has_userpoints) ? $node->has_userpoints : 1,
      '#title' => t('Enable UserPoints Module Integration'),
      '#description' => t('If checked, marks scored in this @quiz will be credited to userpoints. For each correct answer 1 point will be added to user\'s point.', array(
        '@quiz' => QUIZ_NAME,
      )),
    );
  }

  // Set up the availability options.
  $form['quiz_availability'] = array(
    '#type' => 'fieldset',
    '#title' => t('Availability options'),
    '#collapsed' => TRUE,
    '#collapsible' => TRUE,
    '#attributes' => array(
      'id' => 'availability-fieldset',
    ),
  );
  $form['quiz_availability']['quiz_always'] = array(
    '#type' => 'checkbox',
    '#title' => t('Always Available'),
    '#default_value' => $node->quiz_always,
    '#description' => t('Click this option to ignore the open and close dates.'),
  );
  $form['quiz_availability']['quiz_open'] = array(
    '#type' => 'date',
    '#title' => t('Open Date'),
    '#default_value' => _quiz_form_prepare_date($node->quiz_open),
    '#description' => t('The date this @quiz will become available.', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#after_build' => array(
      '_quiz_limit_year_options',
    ),
  );
  $form['quiz_availability']['quiz_close'] = array(
    '#type' => 'date',
    '#title' => t('Close Date'),
    '#default_value' => _quiz_form_prepare_date($node->quiz_close, variable_get('quiz_default_close', 30)),
    '#description' => t('The date this @quiz will cease to be available.', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#after_build' => array(
      '_quiz_limit_year_options',
    ),
  );

  // Quiz summary options.
  $form['summaryoptions'] = array(
    '#type' => 'fieldset',
    '#title' => t('Pass/fail options'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#attributes' => array(
      'id' => 'summaryoptions-fieldset',
    ),
  );

  // If pass/fail option is checked, present the form elements.
  if (variable_get('quiz_use_passfail', 1)) {
    $form['summaryoptions']['pass_rate'] = array(
      '#type' => 'textfield',
      '#title' => t('Pass rate for @quiz (%)', array(
        '@quiz' => QUIZ_NAME,
      )),
      '#default_value' => $node->pass_rate,
      '#description' => t('Pass rate for the @quiz as a percentage score.', array(
        '@quiz' => QUIZ_NAME,
      )),
      '#required' => FALSE,
    );
    $form['summaryoptions']['summary_pass'] = array(
      '#type' => 'textarea',
      '#title' => t('Summary text if passed'),
      '#default_value' => $node->summary_pass,
      '#cols' => 60,
      '#description' => t("Summary for when the user gets enough correct answers to pass the @quiz. Leave blank if you don't want to give different summary text if they passed or if you are not using the 'percent to pass' option above. If you don't use the 'Percentage needed to pass' field above, this text will not be used.", array(
        '@quiz' => QUIZ_NAME,
      )),
    );
    $form['summaryoptions']['format'] = filter_form($node->format, NULL, array(
      'summary_format',
    ));
    _quiz_format_mod($form['summaryoptions']['format']);
  }
  else {
    $form['summaryoptions']['pass_rate'] = array(
      '#type' => 'hidden',
      '#value' => $node->pass_rate,
      '#required' => FALSE,
    );
  }

  // We use a helper to enable the wysiwyg module to add an editor to the
  // textarea.
  $form['summaryoptions']['helper']['summary_default'] = array(
    '#type' => 'textarea',
    '#title' => t('Default summary text'),
    '#default_value' => $node->summary_default,
    '#cols' => 60,
    '#description' => t("Default summary. Leave blank if you don't want to give a summary."),
  );
  $form['summaryoptions']['helper']['format'] = filter_form($node->format, NULL, array(
    'default_summary_format',
  ));
  _quiz_format_mod($form['summaryoptions']['helper']['format']);

  // Number of random questions, max score and tid for random questions are set on
  // the manage questions tab. We repeat them here so that they're not removed
  // if the quiz is being updated.
  $num_rand = isset($node->number_of_random_questions) ? $node->number_of_random_questions : 0;
  $form['number_of_random_questions'] = array(
    '#type' => 'value',
    '#value' => $num_rand,
  );
  $max_score_for_random = isset($node->max_score_for_random) ? $node->max_score_for_random : 0;
  $form['max_score_for_random'] = array(
    '#type' => 'value',
    '#value' => $max_score_for_random,
  );
  $tid = isset($node->tid) ? $node->tid : 0;
  $form['tid'] = array(
    '#type' => 'value',
    '#value' => $tid,
  );
  $options = !empty($node->resultoptions) ? $node->resultoptions : array();
  $num_options = max(count($options), variable_get('quiz_max_result_options', 5));
  if ($num_options > 0) {
    $form['resultoptions'] = array(
      '#type' => 'fieldset',
      '#title' => t('Result Comments'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#tree' => TRUE,
      '#attributes' => array(
        'id' => 'resultoptions-fieldset',
      ),
    );
    for ($i = 0; $i < $num_options; $i++) {
      $option = count($options) > 0 ? array_shift($options) : NULL;

      // grab each option in the array
      $form['resultoptions'][$i] = array(
        '#type' => 'fieldset',
        '#title' => t('Result Option ') . ($i + 1),
        '#collapsible' => TRUE,
        '#collapsed' => FALSE,
      );
      $form['resultoptions'][$i]['option_name'] = array(
        '#type' => 'textfield',
        '#title' => t('The name of the result'),
        '#default_value' => $option['option_name'],
        '#maxlength' => 40,
        '#size' => 40,
      );
      $form['resultoptions'][$i]['option_start'] = array(
        '#type' => 'textfield',
        '#title' => t('Percentage Start Range'),
        '#description' => t('Show this result for scored quizzes in this range (0-100).'),
        '#default_value' => $option['option_start'],
        '#size' => 5,
      );
      $form['resultoptions'][$i]['option_end'] = array(
        '#type' => 'textfield',
        '#title' => t('Percentage End Range'),
        '#description' => t('Show this result for scored quizzes in this range (0-100).'),
        '#default_value' => $option['option_end'],
        '#size' => 5,
      );
      $form['resultoptions'][$i]['option_summary'] = array(
        '#type' => 'textarea',
        '#title' => t('Display text for the result'),
        '#default_value' => $option['option_summary'],
        '#description' => t('Result summary. This is the summary that is displayed when the user falls in this result set determined by his/her responses.'),
      );
      $form['resultoptions'][$i]['format'] = filter_form($node->format, NULL, array(
        'resultoptions',
        $i,
        'filter_format',
      ));
      _quiz_format_mod($form['resultoptions'][$i]['format']);
      if ($option['option_id']) {
        $form['resultoptions'][$i]['option_id'] = array(
          '#type' => 'hidden',
          '#value' => $option['option_id'],
        );
      }
    }
  }
  $form['remember_settings'] = array(
    '#type' => 'checkbox',
    '#title' => t('Remember my settings'),
    '#description' => t('If this box is checked most of the quiz specific settings you have made will be remembered and will be your default settings next time you create a quiz.'),
    '#weight' => 49,
  );
  if (quiz_has_been_answered($node) && (!user_access('manual quiz revisioning') || variable_get('quiz_auto_revisioning', 1))) {
    $node->revision = 1;
    $node->log = t('The current revision has been answered. We create a new revision so that the reports from the existing answers stays correct.');
  }
  return $form;
}

/**
 * Implementation of hook_validate().
 */
function quiz_validate($node) {
  if (!$node->nid && empty($_POST)) {
    return;
  }

  // Don't check dates if the quiz is always available.
  if (!$node->quiz_always) {
    if (mktime(0, 0, 0, $node->quiz_open['month'], $node->quiz_open['day'], $node->quiz_open['year']) > mktime(0, 0, 0, $node->quiz_close['month'], $node->quiz_close['day'], $node->quiz_close['year'])) {
      form_set_error('quiz_close', t('Please make sure the close date is after the open date.'));
    }
  }
  if (!empty($node->pass_rate)) {
    if (!_quiz_is_int($node->pass_rate, 0, 100)) {
      form_set_error('pass_rate', t('The pass rate value must be a number between 0 and 100.'));
    }
  }
  if (isset($node->time_limit)) {
    if (!_quiz_is_int($node->time_limit, 0)) {
      form_set_error('time_limit', t('Time limit must be a non negative interger'));
    }
  }
  if (isset($node->resultoptions) && count($node->resultoptions) > 0) {
    $taken_values = array();
    $num_options = 0;
    foreach ($node->resultoptions as $option) {
      if (!empty($option['option_name'])) {
        $num_options++;
        if (empty($option['option_summary'])) {
          form_set_error('option_summary', t('Option has no summary text.'));
        }
        if ($node->pass_rate && (isset($option['option_start']) || isset($option['option_end']))) {

          // Check for a number between 0-100.
          foreach (array(
            'option_start' => 'start',
            'option_end' => 'end',
          ) as $bound => $bound_text) {
            if (!_quiz_is_int($option[$bound], 0, 100)) {
              form_set_error($bound, t('The range %start value must be a number between 0 and 100.', array(
                '%start' => $bound_text,
              )));
            }
          }

          // Check that range end >= start.
          if ($option['option_start'] > $option['option_end']) {
            form_set_error('option_start', t('The start must be less than the end of the range.'));
          }

          // Check that range doesn't collide with any other range.
          $option_range = range($option['option_start'], $option['option_end']);
          if ($intersect = array_intersect($taken_values, $option_range)) {
            form_set_error('option_start', t('The ranges must not overlap each other. (%intersect)', array(
              '%intersect' => implode(',', $intersect),
            )));
          }
          else {
            $taken_values = array_merge($taken_values, $option_range);
          }
        }
      }
      elseif (!_quiz_is_empty_html($option['option_summary'])) {
        form_set_error('option_summary', t('Option has a summary, but no name.'));
      }
    }
  }
  if ($node->allow_jumping && !$node->allow_skipping) {
    form_set_error('allow_skipping', t('If jumping is allowed skipping also has to be allowed.'));
  }
}

/**
 * Implementation of hook_nodeapi()
 */
function quiz_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {

  // We need to filter on node type to prevent this from overriding any other
  // node.
  if ($node->type == 'quiz') {
    switch ($op) {
      case 'presave':

        // 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 ($aid = actions_function_lookup($node->aid)) {
          $node->aid = $aid;
        }
        if (variable_get('quiz_auto_revisioning', 1)) {
          $node->revision = quiz_has_been_answered($node) ? 1 : 0;
        }
        break;
      case 'prepare':

        // Meet E_STRICT on new nodes.
        $defaults = _quiz_get_node_defaults();
        if (!isset($node->nid)) {
          foreach ($defaults as $key => $value) {
            if (!isset($node->{$key})) {
              $node->{$key} = $value;
            }
          }
        }
        break;
    }
  }
  if (isset($node->is_quiz_question)) {
    if (variable_get('quiz_auto_revisioning', 1) && $op == 'presave') {
      $node->revision = quiz_question_has_been_answered($node) ? 1 : 0;
    }
  }
}

/**
 * Implementation of hook user().
 */
function quiz_user($op, &$edit, &$account, $category = NULL) {
  switch ($op) {
    case 'delete':
      $sql = 'DELETE FROM {quiz_user_settings}
              WHERE uid = %d';
      db_query($sql, $account->uid);
      if (variable_get('quiz_durod', 0)) {
        _quiz_delete_users_results($account->uid);
      }
      break;
  }
}

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

/**
 * @defgroup quiz_public Public API functions.
 * @{
 */

/**
 * 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->type == 'quiz' ? $node : FALSE;
  if ($to_return) {
    $quiz_nodes[$arg] = $to_return;
  }
  return $to_return;
}

/**
 * Validate that a node is of type quiz, and that the user has access to it.
 *
 * @param $node
 * @param $op
 * @return unknown_type
 */
function quiz_type_confirm($node, $op = NULL) {
  if ($node->type != 'quiz') {
    return FALSE;
  }
  if (isset($op)) {
    $node_ops = array(
      'view',
      'update',
      'delete',
      'create',
    );
    if (in_array($op, $node_ops)) {
      return node_access($op, $node);
    }
    return user_access($op);
  }
  return user_access('access quiz');
}

/**
 * Returns the titles for all quizzes the user has access to.
 *
 * @return quizzes
 *   Array with nids as keys and titles as values.
 */
function quiz_get_all_titles() {
  $sql = "SELECT n.nid, n.title\n          FROM {node} n\n          WHERE n.type = 'quiz'";
  $res = db_query(db_rewrite_sql($sql));
  $to_return = array();
  while ($res_o = db_fetch_object($res)) {
    $to_return[$res_o->nid] = $res_o->title;
  }
  return $to_return;
}

/**
 * Returns the titles for all quizzes the user has access to.
 *
 * @return quizzes
 *   Array with nids as keys and (array with vid as key and title as value) as values.
 *   Like this: array($nid => array($vid => $title))
 */
function quiz_get_all_version_titles() {
  $sql = "SELECT r.nid, r.vid, r.title\n          FROM {node_revisions} r\n          LEFT JOIN {node} n ON n.nid = r.nid\n          WHERE n.type = 'quiz'";
  $res = db_query(db_rewrite_sql($sql));
  $to_return = array();
  while ($res_o = db_fetch_object($res)) {
    $to_return[$res_o->nid][$res_o->vid] = $res_o->title;
  }
  return $to_return;
}

/**
 * Return highest score data for given quizzes.
 *
 * @param $nids
 *   nids for the quizzes we want to collect scores from.
 * @param $uid
 *   uid for the user we want to collect score for.
 * @param $include_num_questions
 *   Do we want to collect information about the number of questions in a quiz?
 *   This adds a performance hit.
 * @return
 *   Array of score data.
 *   For several takes on the same quiz, only returns highest score.
 */
function quiz_get_score_data($nids, $uid, $include_num_questions = FALSE) {

  // Validate that the nids are integers.
  foreach ($nids as $key => $nid) {
    if (!_quiz_is_int($nid)) {
      unset($nids[$key]);
    }
  }
  if (empty($nids)) {
    return array();
  }

  // Fetch score data for the validated nids.
  $to_return = array();
  $vids = array();
  $sql = 'SELECT n.title, n.nid, n.vid, p.number_of_random_questions as num_random_questions, r.score AS percent_score, p.max_score, p.pass_rate AS percent_pass
          FROM {node} n
          JOIN {quiz_node_properties} p
          ON n.vid = p.vid
          LEFT OUTER JOIN {quiz_node_results} r
          ON r.nid = n.nid AND r.uid = %d
          LEFT OUTER JOIN (
            SELECT nid, max(score) as highest_score
            FROM {quiz_node_results}
            GROUP BY nid
          ) rm
          ON n.nid = rm.nid AND r.score = rm.highest_score
          WHERE n.nid in (' . implode(', ', $nids) . ')
          ';
  $res = db_query($sql, $uid);
  while ($res_o = db_fetch_object($res)) {
    if (!$include_num_questions) {
      unset($res_o->num_random_questions);
    }
    if (!isset($to_return[$res_o->vid]) || $res_o->percent_score > $to_return[$res_o->vid]->percent_score) {
      $to_return[$res_o->vid] = $res_o;

      // Fetch highest score
    }

    // $vids will be used to fetch number of questions.
    $vids[] = $res_o->vid;
  }
  if (empty($vids)) {
    return array();
  }

  // Fetch total number of questions.
  if ($include_num_questions) {
    $sql = 'SELECT COUNT(*) AS num_always_questions, parent_vid
            FROM {quiz_node_relationship}
            WHERE parent_vid IN (' . implode(', ', $vids) . ')
            AND question_status = %d
            GROUP BY parent_vid';
    $res = db_query(db_rewrite_sql($sql), QUESTION_ALWAYS);
    while ($res_o = db_fetch_object($res)) {
      $to_return[$res_o->parent_vid]->num_questions = $to_return[$res_o->parent_vid]->num_random_questions + $res_o->num_always_questions;
    }
  }
  return $to_return;
}

/**
 * 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_result(db_query('SELECT number_of_random_questions FROM {quiz_node_properties} WHERE vid = %d', $vid));
  return $always_count + (int) $rand_count;
}

/**
 * Finds out the pass rate for the quiz.
 *
 * @param $nid
 *   The quiz ID.
 * @return
 *   Returns the passing percentage of the quiz.
 */
function quiz_get_pass_rate($nid, $vid) {
  return db_result(db_query('SELECT pass_rate FROM {quiz_node_properties} WHERE nid = %d AND vid = %d', $nid, $vid));
}

/**
 * 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) {
  $sql = "INSERT INTO {quiz_node_relationship} (parent_nid, parent_vid, child_nid, child_vid, question_status, weight, max_score)\n          SELECT src.parent_nid, %d, src.child_nid, src.child_vid, src.question_status, src.weight, src.max_score\n          FROM {quiz_node_relationship} AS src\n          WHERE src.parent_vid = %d AND src.parent_nid = %d AND src.question_status != %d";
  db_query($sql, $new_quiz_vid, $old_quiz_vid, $quiz_nid, QUESTION_NEVER);
  $sql = "INSERT INTO {quiz_terms} (nid, vid, tid, weight, max_score, number)\n          SELECT qt.nid, %d, qt.tid, qt.weight, qt.max_score, qt.number\n          FROM {quiz_terms} qt\n          WHERE qt.vid = %d";
  db_query($sql, $new_quiz_vid, $old_quiz_vid);
}

/**
 * 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($quiz) {
  global $user;
  $allow_skipping = $quiz->allow_skipping;
  if (!isset($quiz)) {
    drupal_not_found();
    return;
  }

  // If anonymous user and no unique hash, refresh with a unique string to
  // prevent caching.
  if (!$user->uid && arg(4) != NULL) {
    drupal_goto('node/' . $quiz->nid . '/take/' . md5(mt_rand() . time()));
  }

  // Make sure we use the same revision of the quiz throughout the quiz taking
  // session.
  if (isset($_SESSION['quiz_' . $quiz->nid]['quiz_vid']) && $quiz->vid != $_SESSION['quiz_' . $quiz->nid]['quiz_vid']) {
    $quiz = node_load($quiz->nid, $_SESSION['quiz_' . $quiz->nid]['quiz_vid']);
  }

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

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

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

    // Are we resuming an in-progress quiz?
    if ($quiz->allow_resume && $rid > 0) {
      _quiz_resume_existing_quiz($quiz, $user->uid, $rid);
    }
    elseif (quiz_start_check($quiz, $rid)) {

      // 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->nid]['result_id'] = quiz_create_rid($quiz);
      $_SESSION['quiz_' . $quiz->nid]['quiz_questions'] = $questions;
      $_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions'] = array();
      $_SESSION['quiz_' . $quiz->nid]['question_number'] = 0;
      $_SESSION['quiz_' . $quiz->nid]['question_start_time'] = time();
      $_SESSION['quiz_' . $quiz->nid]['question_duration'] = $quiz->time_limit;
      $_SESSION['quiz_' . $quiz->nid]['quiz_vid'] = $quiz->vid;
    }
    else {
      return array(
        'body' => array(
          '#value' => 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(
        '#value' => t('This quiz is closed'),
      ),
    );
  }
  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->nid]['quiz_questions'][0]['nid']) {

    // The user has pressed the navigation buttons multiple times...
  }
  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->nid]['quiz_questions'][0]['rid'] = $_SESSION['quiz_' . $quiz->nid]['result_id'];
    $_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions'][] = $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0];
    $former_question_array = array_shift($_SESSION['quiz_' . $quiz->nid]['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->type]['module'];
    $result = module_invoke($module, 'evaluate_question', $former_question, $_SESSION['quiz_' . $quiz->nid]['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->nid]['result_id']);
      $path = drupal_get_path('module', 'quiz');
      require_once $path . '/quiz.pages.inc';
      if ($report) {
        $_SESSION['quiz_' . $quiz->nid]['feedback'] = rawurlencode(drupal_get_form('quiz_report_form', array(
          $report,
        )));
      }
    }
    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->nid]['previous_quiz_questions']);
        array_unshift($_SESSION['quiz_' . $quiz->nid]['quiz_questions'], $last_q);
        drupal_set_message(t('The answer was incorrect. Please try again.'), 'error');
        unset($_SESSION['quiz_' . $quiz->nid]['feedback']);
      }
    }
    elseif ($_POST['op'] == t('Back') && $quiz->backwards_navigation) {
      $quiz_id = 'quiz_' . $quiz->nid;

      // 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->uid && $q_passed_validation === TRUE) {
      drupal_goto('node/' . $quiz->nid . '/take', array(
        'quizkey' => md5(mt_rand() . time()),
      ));
    }
  }

  // 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->nid]['result_id'])) {
      $_SESSION['quiz_' . $quiz->nid]['result_id'] = quiz_create_rid($quiz);
    }
    $q_passed_validation = TRUE;

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

      // Load the last asked question.
      $former_question_array = array_shift($_SESSION['quiz_' . $quiz->nid]['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->type);
    if (!$module) {
      return array(
        'body' => array(
          '#value' => ' ',
        ),
      );
    }
    $result = module_invoke($module, 'skip_question', $former_question, $_SESSION['quiz_' . $quiz->nid]['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->nid]['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->nid]['quiz_questions']) || is_string($q_passed_validation)) {

    // 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->nid]['quiz_questions'][0]['nid'], $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['vid']);
      if (isset($_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['rid'])) {
        $question_node->rid = $_SESSION['quiz_' . $quiz->nid]['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->nid]['quiz_questions'], $former_question_array);
      if (is_array($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions'])) {
        array_pop($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']);
      }

      // Avoid caching for anonymous users
      if (!$user->uid) {
        drupal_set_message($q_passed_validation, 'error');
        drupal_goto('node/' . $quiz->nid . '/take', array(
          'quizkey' => md5(mt_rand() . time()),
        ));
      }
    }

    // Added the progress info to the view.
    $number_of_questions = quiz_get_number_of_questions($quiz->vid);
    $question_number = $number_of_questions - count($_SESSION['quiz_' . $quiz->nid]['quiz_questions']);
    $question_node->question_number = $question_number;
    $content['progress']['#value'] = theme('quiz_progress', $question_number, $number_of_questions, $quiz->allow_jumping, $quiz->time_limit);
    $content['progress']['#weight'] = -50;
    if (count($_SESSION['quiz_' . $quiz->nid]['quiz_questions']) + count($_SESSION['quiz_' . $quiz->nid]['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->nid]);
      drupal_goto('node/' . $quiz->nid . '/take');
    }
    if ($_SESSION['quiz_' . $quiz->nid]['question_duration']) {
      $_SESSION['quiz_' . $quiz->nid]['question_duration'] -= time() - $_SESSION['quiz_' . $quiz->nid]['question_start_time'];
      $time = $_SESSION['quiz_' . $quiz->nid]['question_duration'] > 0 ? $_SESSION['quiz_' . $quiz->nid]['question_duration'] : 1;
      db_query("UPDATE {quiz_node_results} SET time_left = %d WHERE result_id = %d", $time, $_SESSION['quiz_' . $quiz->nid]['result_id']);
      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->nid]['quiz_questions']) + count($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']) + 1, $quiz, $_SESSION['quiz_' . $quiz->nid]['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') && variable_get('quiz_has_timer', 1)) {
          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_to_js(t('Finish'));
          $button_op2 = drupal_to_js(t('Next'));
          $js = "\n            function finished() {\n              // Find all buttons with a name of 'op'.\n              var buttons = \$('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, 'inline');
        }
      }
      $_SESSION['quiz_' . $quiz->nid]['question_start_time'] = 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']['#value'] = 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->nid]['feedback']) && $quiz->feedback_time == QUIZ_FEEDBACK_QUESTION) {
        $content['body']['feedback']['#value'] = rawurldecode($_SESSION['quiz_' . $quiz->nid]['feedback']);
        $content['body']['feedback']['#weight'] = -100;
      }
      drupal_set_title(check_plain($quiz->title));
      unset($_SESSION['quiz_' . $quiz->nid]['feedback']);
    }
  }
  else {
    drupal_set_title(filter_xss($quiz->title));
    $quiz_end = TRUE;
  }

  // If we're at the end of the quiz.
  if (!empty($quiz_end)) {

    // 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->nid]['result_id']);
    $score = quiz_end_scoring($quiz, $_SESSION['quiz_' . $quiz->nid]['result_id']);
    if ($quiz->feedback_time == QUIZ_FEEDBACK_NEVER) {
      $content['body']['#value'] = theme('quiz_no_feedback');
    }
    else {

      // Get the results and summary text for this quiz.
      $summary = _quiz_get_summary_text($quiz, $score);

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

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

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

/**
 * 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->nid]['quiz_questions']);
  $num_previous = count($_SESSION['quiz_' . $quiz->nid]['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->nid]['quiz_questions'], array_pop($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']));
    }
  }
  elseif ($question_num > $num_previous + 1) {
    for ($i = 0; $i < $question_num - $num_previous - 1; $i++) {
      $_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions'][] = array_shift($_SESSION['quiz_' . $quiz->nid]['quiz_questions']);
    }
    _quiz_cleanup_after_jumping($quiz, $rid);
  }
}

/**
 * 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;
  $question_node = node_build_content($question_node, FALSE, TRUE);
  node_invoke_nodeapi($question_node, 'alter', FALSE, TRUE);
  $question_node->body = drupal_render($question_node->content);
  return theme('quiz_single_question_node', $question_node);
}

/**
 * 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) {
  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_result(db_query("SELECT (max_score / (\n                  SELECT max_score\n                  FROM {quiz_question_properties}\n                  WHERE nid = %d AND vid = %d\n                )) as scale\n                FROM {quiz_node_relationship}\n                WHERE parent_nid = %d\n                AND parent_vid = %d\n                AND child_nid = %d\n                AND child_vid = %d\n               ", $result->nid, $result->vid, $quiz->nid, $quiz->vid, $result->nid, $result->vid));
  }
  elseif ($quiz->randomization == 2) {
    $scale = db_result(db_query("SELECT (max_score_for_random / (\n                  SELECT max_score\n                  FROM {quiz_question_properties}\n                  WHERE nid = %d AND vid = %d\n                )) as scale\n                FROM {quiz_node_properties}\n                WHERE vid = %d\n               ", $result->nid, $result->vid, $quiz->vid));
  }
  elseif ($quiz->randomization == 3) {
    if (isset($options['question_data']['tid'])) {
      $result->tid = $options['question_data']['tid'];
    }
    $scale = db_result(db_query("SELECT (max_score / (\n                  SELECT max_score\n                  FROM {quiz_question_properties}\n                  WHERE nid = %d AND vid = %d\n                )) as scale\n                FROM {quiz_terms}\n                WHERE vid = %d\n                AND tid = %d\n               ", $result->nid, $result->vid, $quiz->vid, $result->tid));
  }
  $points = round($result->score * $scale);

  // Insert result data, or update existing data.
  $rid_count = db_result(db_query("SELECT COUNT('result_id') AS count\n              FROM {quiz_node_results_answers}\n              WHERE question_nid = %d\n              AND question_vid = %d\n              AND result_id = %d", $result->nid, $result->vid, $result->rid));
  if ($rid_count > 0) {
    $sql = "UPDATE {quiz_node_results_answers}\n            SET is_correct = %d, points_awarded = %d, answer_timestamp = %d, is_skipped = %d";
    $args = array(
      $result->is_correct,
      $points,
      time(),
      $result->is_skipped,
    );
    if ($quiz->randomization == 3) {
      $sql .= ", tid = %d";
      $args[] = $result->tid;
    }
    $sql .= " WHERE question_nid = %d AND question_vid = %d AND result_id = %d";
    array_push($args, $result->nid, $result->vid, $result->rid);
    db_query($sql, $args);
  }
  else {
    $sql = "INSERT INTO {quiz_node_results_answers}\n            (question_nid, question_vid, result_id, is_correct, points_awarded,\n            answer_timestamp, is_skipped, number";
    if ($quiz->randomization == 3) {
      $sql .= ", tid";
    }
    $sql .= ") VALUES (%d, %d, %d, %d, %d, %d, %d, %d";
    $args = array(
      $result->nid,
      $result->vid,
      $result->rid,
      $result->is_correct,
      $points,
      time(),
      $result->is_skipped,
      $options['question_data']['number'],
    );
    if ($quiz->randomization == 3) {
      $sql .= ", %d";
      $args[] = $result->tid;
    }
    $sql .= ')';
    db_query($sql, $args);
  }
}

/**
 * Score a completed quiz.
 */
function quiz_end_scoring($quiz, $rid) {
  global $user;
  $score = quiz_calculate_score($quiz, $rid);
  if (!isset($score['percentage_score'])) {
    $score['percentage_score'] = 0;
  }
  db_query("UPDATE {quiz_node_results}\n            SET is_evaluated = %d,\n            time_end = %d,\n            score = %d\n            WHERE result_id = %d", $score['is_evaluated'], time(), $score['percentage_score'], $rid);
  if ($user->uid) {
    $score['passing'] = quiz_is_passed($user->uid, $quiz->nid, $quiz->vid);
  }
  else {
    $score['passing'] = $score['percentage_score'] >= $quiz->pass_rate;
  }
  return $score;
}

/**
 * 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, $rid, $score) {

  // Call hook_quiz_finished().
  module_invoke_all('quiz_finished', $quiz, $score, $rid);

  // 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.
    $context = array(
      'result_id' => $rid,
      'score' => $score,
    );
    actions_do($quiz->aid, $quiz, $context, $score);

    // WARNING: MIGHT GET REDIRECTED HERE
  }
  return $score;
}

/**
 * Implementation hook_quiz_finished().
 *
 * Performs actions like sending quiz results over email at the end of quiz.
 */
function quiz_quiz_finished($quiz, $score, $rid) {

  // Load data about the quiz taker
  $sql = 'SELECT u.uid, u.mail FROM {users} u JOIN {quiz_node_results} qnr ON u.uid = qnr.uid WHERE result_id = %d';
  $taker = db_fetch_object(db_query($sql, $rid));
  if (variable_get('quiz_results_to_quiz_author', 0)) {
    $author_mail = db_result(db_query('SELECT mail FROM {users} WHERE uid = %d', $quiz->uid));
    drupal_mail('quiz', 'notice', $author_mail, NULL, array(
      $quiz,
      $score,
      $rid,
      'author',
    ));
  }
  if (variable_get('quiz_email_results', 0) && variable_get('quiz_use_passfail', 1) && $taker->uid != 0 && $score['is_evaluated']) {
    drupal_mail('quiz', 'notice', $taker->mail, NULL, array(
      $quiz,
      $score,
      $rid,
      'taker',
    ));
    drupal_set_message(t('Your results have been sent to your e-mail address.'));
  }

  // Calls userpoints functions to credit user point based on number of correct
  // answers.
  if ($quiz->has_userpoints && $taker->uid != 0 && $score['is_evaluated']) {

    //Looking up the tid of the selected Userpoint vocabulary
    $selected_tid = db_result(db_query("SELECT tid FROM {term_node}\n                WHERE nid = %d AND vid = %d AND tid IN (\n                  SELECT tid\n                  FROM {term_data}\n                  WHERE vid = %d\n                )", $quiz->nid, $quiz->vid, userpoints_get_vid()));
    $variables = array(
      '@title' => $quiz->title,
      '@quiz' => variable_get('quiz_name', QUIZ_NAME),
      '@time' => date('l jS \\of F Y h:i:s A'),
    );
    $params = array(
      'points' => $score['numeric_score'],
      'description' => t('Attended @title @quiz on @time', $variables),
      'tid' => $selected_tid,
      'uid' => $taker->uid,
    );
    userpoints_userpointsapi($params);
  }
}

/**
 * Implementation hook_quiz_scored().
 *
 * Performs actions like sending quiz results over email at the end of quiz.
 */
function quiz_quiz_scored($quiz, $score, $rid) {
  $sql = 'SELECT u.uid, u.mail FROM {users} u JOIN {quiz_node_results} qnr ON u.uid = qnr.uid WHERE result_id = %d';
  $taker = db_fetch_object(db_query($sql, $rid));
  if (variable_get('quiz_email_results', 0) && $taker->uid != 0 && $score['is_evaluated']) {
    drupal_mail('quiz', 'notice', $taker->mail, NULL, array(
      $quiz,
      $score,
      $rid,
      'taker',
    ));
    drupal_set_message(t('The results has been sent to the users e-mail address.'));
  }

  // Calls userpoints functions to credit user point based on number of correct
  // answers.
  if ($quiz->has_userpoints && $taker->uid != 0 && $score['is_evaluated']) {

    //Looking up the tid of the selected Userpoint vocabulary
    $selected_tid = db_result(db_query("SELECT tid FROM {term_node}\n                WHERE nid = %d AND vid = %d AND tid IN (\n                  SELECT tid\n                  FROM {term_data}\n                  WHERE vid = %d\n                )", $quiz->nid, $quiz->vid, userpoints_get_vid()));
    $variables = array(
      '@title' => $quiz->title,
      '@quiz' => variable_get('quiz_name', QUIZ_NAME),
      '@time' => date('l jS \\of F Y h:i:s A'),
    );
    $params = array(
      'points' => $score['numeric_score'],
      'description' => t('Attended @title @quiz on @time', $variables),
      'tid' => $selected_tid,
    );
    userpoints_userpointsapi($params);
  }
}

/**
 * Implementation of hook_mail().
 */
function quiz_mail($key, &$message, $params) {
  global $user;
  list($quiz, $score, $rid, $target) = $params;
  $substitutions = array(
    '!title' => $quiz->title,
    '!sitename' => variable_get('site_name', 'Quiz'),
    '!taker' => $user->name,
    '!author' => $quiz->name,
    '!title' => check_plain($quiz->title),
    '!date' => date("F j, Y, g:i a"),
    '!desc' => check_plain($quiz->body),
    '!correct' => isset($score['numeric_score']) ? $score['numeric_score'] : 0,
    '!total' => $score['possible_score'],
    '!percentage' => $score['percentage_score'],
    '!url' => url('user/quiz/' . $rid . '/userresults', array(
      'absolute' => TRUE,
    )),
    '!minutes' => db_result(db_query("SELECT CEIL((time_end - time_start)/60) FROM {quiz_node_results} WHERE result_id = '%d' AND time_end", $rid)),
  );
  switch ($key) {
    case 'notice':
      if ($target == 'author') {
        $subject = t(variable_get('quiz_email_results_subject', quiz_email_results_format('subject', 'author')), $substitutions, $user->language);
        $body = t(variable_get('quiz_email_results_body', quiz_email_results_format('body', 'author')), $substitutions, $user->language);
      }
      else {
        $subject = t(variable_get('quiz_email_results_subject_taker', quiz_email_results_format('subject', 'taker')), $substitutions, $user->language);
        $body = t(variable_get('quiz_email_results_body_taker', quiz_email_results_format('body', 'taker')), $substitutions, $user->language);
      }
      $message['subject'] = $subject;
      $message['body'] = $body;
      break;
  }
}

/**
 * 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) {
  global $user;
  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";
    }
  }
}

/**
 * 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_query('UPDATE {quiz_node_results} SET score = %d WHERE result_id = %d', $score['percentage_score'], $rid);
  if ($score['is_evaluated']) {

    // Call hook_quiz_scored().
    module_invoke_all('quiz_scored', $quiz, $score, $rid);
    _quiz_maintain_results($quiz, $rid);
    db_query('UPDATE {quiz_node_results} SET is_evaluated = 1 WHERE result_id = %d', $rid);
  }
  return $score['percentage_score'];
}

/**
 * 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;
  }
  $sql = "UPDATE {quiz_node_properties} qnp\n          SET max_score = max_score_for_random * number_of_random_questions + (\n            SELECT SUM(max_score)\n            FROM {quiz_node_relationship} qnr\n            WHERE qnr.question_status = " . QUESTION_ALWAYS . "\n            AND parent_vid = qnp.vid\n          )\n          WHERE vid IN( " . db_placeholders($quizzes_to_update) . ")";
  db_query($sql, $quizzes_to_update);
  $sql = "UPDATE {quiz_node_properties} qnp\n          SET qnp.max_score = (\n            SELECT SUM(qt.max_score * qt.number)\n            FROM {quiz_terms} qt\n            WHERE qt.nid = qnp.nid AND qt.vid = qnp.vid\n          )\n          WHERE qnp.randomization = 3 AND qnp.vid IN( " . db_placeholders($quizzes_to_update) . ")";
  db_query($sql, $quizzes_to_update);

  // No need to use db_placeholders for time()...
  $sql = "UPDATE {node_revisions}\n          SET timestamp = " . time() . "\n          WHERE vid IN( " . db_placeholders($quizzes_to_update) . ")";
  db_query($sql, $quizzes_to_update);
  $sql = "UPDATE {node}\n          SET changed = " . time() . "\n          WHERE vid IN( " . db_placeholders($quizzes_to_update) . ")";
  db_query($sql, $quizzes_to_update);

  // Update percent scores
  $sql = 'UPDATE {quiz_node_results} r
          SET r.score = ROUND(
            100 * (
              SELECT SUM(a.points_awarded)
              FROM {quiz_node_results_answers} a
              WHERE a.result_id = r.result_id
            ) / (
              SELECT max_score
              FROM {quiz_node_properties} qnp
              WHERE qnp.vid = r.vid
            )
          )
          WHERE r.vid IN(' . db_placeholders($quizzes_to_update) . ')';
  db_query($sql, $quizzes_to_update);
}

/**
 * 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_result(db_query("SELECT COUNT(result_id) AS passed_count\n    FROM {quiz_node_results} qnrs\n    INNER JOIN {quiz_node_properties} USING (vid, nid)\n    WHERE qnrs.vid = %d AND qnrs.nid = %d AND qnrs.uid =%d AND score >= pass_rate", $vid, $nid, $uid));

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

/**
 * 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) {
  global $user;
  $user_is_admin = user_access('edit any quiz') || user_access('edit own quiz') && $quiz->uid == $user->uid;

  // 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_result(db_query($query, $user->uid, $quiz->nid));
    $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 ($user->uid && quiz_is_passed($user->uid, $quiz->nid, $quiz->vid)) {
    drupal_set_message(t('You have already passed this @quiz.', array(
      '@quiz' => QUIZ_NAME,
    )), 'status');
  }

  // Call hook_quiz_begin().
  module_invoke_all('quiz_begin', $quiz, $rid);
  return TRUE;
}

/**
 * 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($quiz) {
  global $user;
  if ($user->uid == 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_access('edit any quiz') || user_access('edit own quiz') && $quiz->uid == $user->uid;
  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;
}

/**
 * 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) {
  global $user;
  $result = db_query("INSERT INTO {quiz_node_results} (nid, vid, uid, time_start) VALUES (%d, %d, %d, %d)", $quiz->nid, $quiz->vid, $user->uid, time());
  if ($result) {

    // Return the last RID.
    $rid = db_last_insert_id('quiz_node_results', 'result_id');
  }
  else {
    drupal_set_message(t('There was a problem starting the @quiz. Please try again later.', array(
      '@quiz' => QUIZ_NAME,
    ), 'error'));
    return FALSE;
  }
  return $rid;
}

/**
 * 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
  $sql = 'SELECT a.question_nid, a.question_vid, n.type, r.max_score, qt.max_score as term_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 = %d)
  LEFT OUTER JOIN {quiz_terms} qt ON (a.tid = qt.tid AND qt.vid = %d)
  WHERE a.result_id = %d';
  $results = db_query($sql, $quiz->vid, $quiz->vid, $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;
  while ($question = db_fetch_object($results)) {

    // Questions picked from term id's won't be found in the quiz_node_relationship table
    if ($question->max_score === NULL) {
      if ($quiz->randomization == 3 && $question->term_max_score !== NULL) {
        $question->max_score = $question->term_max_score;
      }
      elseif (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);
      if ($score->possible != $question->max_score) {
        $score_weight = $score->possible > 0 ? $question->max_score / $score->possible : 0;
        $score->possible = $question->max_score;
        $score->attained *= $score_weight;
      }
      $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,
  );
}

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

/**
 * 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.
  $sql = "SELECT child_nid as nid, child_vid as vid, max_score as relative_max_score\n    FROM {quiz_node_relationship} qnr\n    JOIN {node} n ON qnr.child_nid = n.nid\n    WHERE qnr.parent_vid = %d\n    AND qnr.question_status = %d\n    AND n.status = 1\n    ORDER BY weight";
  $result = db_query($sql, $quiz->vid, QUESTION_ALWAYS);
  while ($question_node = db_fetch_array($result)) {
    $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;
}

/**
 * Builds the questionlist for quizzes with categorized random questions
 *
 * @see quiz_build_question_list()
 */
function _quiz_build_categorized_question_list($quiz) {
  $terms = _quiz_get_terms($quiz->vid);
  $questions = array();
  $nids = array();
  $question_types = array_keys(_quiz_get_question_types());
  if (empty($question_types)) {
    return array();
  }
  $total_count = 0;
  foreach ($terms as $term) {
    $sql = "SELECT n.nid, n.vid, tn.tid\n            FROM {node} n\n            JOIN {term_node} tn USING(vid)\n            WHERE n.status = 1\n            AND n.type IN('" . implode("','", $question_types) . "')\n            AND tn.tid = %d";

    // $question_types isn't user input. It is considered safe.
    if (!empty($nids)) {
      $sql .= " AND n.nid NOT IN(" . implode(', ', $nids) . ")";
    }
    $sql .= " ORDER BY RAND()";
    $res = db_query_range($sql, $term->tid, 0, $term->number);
    $count = 0;
    while ($question = db_fetch_array($res)) {
      $count++;
      $question['tid'] = $term->tid;
      $question['number'] = $count + $total_count;
      $questions[] = $question;
      $nids[] = $question['nid'];
    }
    $total_count += $count;
    if ($count < $term->number) {

      // Not enough questions
      return FALSE;
    }
  }
  return $questions;
}

/**
 * Get data for all terms belonging to a Quiz with categorized random questions
 *
 * @param int $vid
 *  version id for the quiz
 * @return array
 *  Array with all terms that belongs to the quiz as objects
 */
function _quiz_get_terms($vid) {
  $sql = 'SELECT td.name, qt.*
          FROM {quiz_terms} qt
          JOIN {term_data} td ON qt.tid = td.tid
          WHERE qt.vid = %d
          ORDER BY qt.weight';
  $res = db_query($sql, $vid);
  $to_return = array();
  while ($res_o = db_fetch_object($res)) {
    $to_return[] = $res_o;
  }
  return $to_return;
}

/**
 * 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->format, FALSE);
  }
  $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;
  }
  $new_question->weight = $node->weight;
  return $new_question;
}

/**
 * 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->vid;
  if ($set_new_revision) {

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

  // 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_query('DELETE FROM {quiz_node_relationship} WHERE parent_nid = %d AND parent_vid = %d', $quiz->nid, $quiz->vid);
  if (empty($questions)) {
    return TRUE;

    // This is not an error condition.
  }

  // Now we do an insert of everything in the quiz. (Note that we are using a
  // subselect to get the most recent vid.)
  $refresh_sql = "INSERT INTO {quiz_node_relationship}\n                  (parent_nid, parent_vid, child_nid, child_vid, question_status, weight, max_score)\n                  VALUES (%d, %d, %d, (SELECT vid FROM {node} WHERE nid = %d), %d, %d, %d)";
  $norefresh_sql = "INSERT INTO {quiz_node_relationship}\n                    (parent_nid, parent_vid, child_nid, child_vid, question_status, weight, max_score)\n                    VALUES (%d, %d, %d, %d, %d, %d, %d)";
  foreach ($questions as $question) {
    if ($question->state != QUESTION_NEVER) {
      if ($question->refresh) {
        $result = db_query($refresh_sql, $quiz->nid, $quiz->vid, $question->nid, $question->nid, $question->state, $question->weight, $question->max_score);
      }
      else {
        $result = db_query($norefresh_sql, $quiz->nid, $quiz->vid, $question->nid, $question->vid, $question->state, $question->weight, $question->max_score);
      }
    }
  }
  quiz_update_max_score_properties(array(
    $quiz->vid,
  ));
  return TRUE;
}

/**
 * @} End of "defgroup quiz_public".
 */

/**
 * 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.
  $sql = "SELECT question_nid AS nid, question_vid AS vid, number\n          FROM {quiz_node_results_answers}\n          WHERE result_id = %d\n          ORDER BY number, answer_timestamp";
  $answered_questions = db_query($sql, $rid);
  while ($answered = db_fetch_object($answered_questions)) {
    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) && $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->nid]['previous_quiz_questions'] = array();
  $_SESSION['quiz_' . $quiz->nid]['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->nid]['previous_quiz_questions'][] = $question;
    }
    else {
      $question['number'] = $next_number;
      $next_number++;
      $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][] = $question;
    }
  }
  if (empty($_SESSION['quiz_' . $quiz->nid]['quiz_questions']) && count($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']) == count($already_answered)) {
    $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][] = array_pop($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']);
  }
  $_SESSION['quiz_' . $quiz->nid]['result_id'] = $rid;
  $_SESSION['quiz_' . $quiz->nid]['question_number'] = count($_SESSION['quiz_' . $quiz->nid]['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->nid]['question_start_time'] = time();
  $_SESSION['quiz_' . $quiz->nid]['question_duration'] = $quiz->time_limit;
  drupal_set_message(t('Resuming a previous quiz in-progress.'), 'status');
}

/**
 * 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) {
  $sql = "SELECT qnr.result_id\n          FROM {quiz_node_results} qnr\n          WHERE qnr.uid = %d\n          AND qnr.nid = %d\n          AND qnr.time_end = 0\n          AND qnr.vid < %d";
  $res = db_query($sql, $uid, $quiz->nid, $quiz->vid);
  $rids = array();
  while ($rids[] = db_result($res)) {
  }
  quiz_delete_results($rids);
}

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

  // Get any quiz that is open, for this user, and has not already
  // been completed.
  $sql = "SELECT result_id\n          FROM {quiz_node_results} qnr\n          INNER JOIN {quiz_node_properties} qnp ON qnr.vid = qnp.vid\n          WHERE (qnp.quiz_always = 1 OR (%d BETWEEN qnp.quiz_open AND qnp.quiz_close))\n          AND qnr.vid = %d\n          AND qnr.uid = %d\n          AND qnr.time_end = 0";
  $rid = db_result(db_query($sql, $now, $vid, $uid));
  return (int) $rid;
}

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

  // We insert the result options one by one.
  foreach ($node->resultoptions as $id => $option) {
    if ($option['option_name']) {
      $option['nid'] = $node->nid;
      $option['vid'] = $node->vid;
      _quiz_insert_result_option($option);
    }
  }
}

/**
 * Insert one result option.
 *
 * @param $option
 *  The option array to insert.
 */
function _quiz_insert_result_option($option) {
  $sql = "INSERT INTO {quiz_node_result_options} (nid, vid, option_name, option_summary, option_start, option_end)\n    VALUES(%d, %d, '%s', '%s', %d, %d)";
  $values = array(
    $option['nid'],
    $option['vid'],
    $option['option_name'],
    $option['option_summary'],
    $option['option_start'],
    $option['option_end'],
  );
  db_query($sql, $values);
}

/**
 * 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.
  $sql = 'DELETE FROM {quiz_node_result_options}
          WHERE vid = %d';
  db_query($sql, $node->vid);
  _quiz_insert_resultoptions($node);
}

/**
 * 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->format, FALSE);
    }
    else {
      $summary['result'] = check_markup(_quiz_pick_result_option($quiz->nid, $quiz->vid, $score['percentage_score']), $quiz->format, FALSE);
    }
  }

  // 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 (trim($quiz->summary_pass) != '') {
      $summary['passfail'] = check_markup($quiz->summary_pass, $quiz->format, FALSE);
    }
  }
  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->format, FALSE);
    }
  }
  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_result(db_query('SELECT option_summary
       FROM {quiz_node_result_options}
       WHERE nid = %d
       AND vid = %d
       AND %d BETWEEN option_start AND option_end', $qnid, $qvid, $score));
}

/**
 * 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 = %d\n                                AND qnr.parent_nid = %d\n                                AND qnr.question_status = %d\n                                AND n.status = 1\n                                ORDER BY RAND()", $quiz->vid, $quiz->nid, QUESTION_RANDOM, 0, $num_random);
      while ($question_node = db_fetch_array($result)) {
        $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 = taxonomy_get_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.
  $result = db_query_range("SELECT n.nid, n.vid\n    FROM {node} n\n    INNER JOIN {term_node} 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()", 0, $num_random);
  $questions = array();
  while ($question_node = db_fetch_array($result)) {
    $question_node['random'] = TRUE;
    $questions[] = $question_node;
  }
  return $questions;
}

/**
 * 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..
  $to_return = module_invoke_all('quiz_question_info');
  if (empty($to_return)) {
    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();
  }

  // We must make sure names and descriptions are customizable and
  // translationable
  $types = array();
  $placeholders = array();
  foreach ($to_return as $key => $value) {
    $types[] = $key;
  }
  $sql = "SELECT type, name, description\n          FROM {node_type}\n          WHERE type IN ('" . implode("', '", $types) . "')";
  $res = db_query($sql, $types);
  while ($res_o = db_fetch_object($res)) {
    $to_return[$res_o->type]['name'] = check_plain($res_o->name);
    $to_return[$res_o->type]['description'] = check_markup($res_o->description);
  }
  return $to_return;
}

/**
 * Retrieve list of vocabularies for all quiz question types.
 *
 * @return
 *   An array containing a vocabulary list.
 */
function _quiz_get_vocabularies() {
  $vocabularies = array();
  $types = array_keys(_quiz_get_question_types());
  foreach ($types as $type) {
    foreach (taxonomy_get_vocabularies($type) as $vid => $vocabulary) {
      $vocabularies[$vid] = $vocabulary;
    }
  }
  return $vocabularies;
}

/**
 * Prints a taxonomy selection form for each vocabulary.
 *
 * @param $value
 *   Default selected value(s).
 * @return
 *   HTML output to print to screen.
 */
function _quiz_taxonomy_select($value = 0) {
  $options = array();
  foreach (_quiz_get_vocabularies() as $vid => $vocabulary) {
    $temp = taxonomy_form($vid, $value);
    $options = array_merge($options, $temp['#options']);
  }
  return $options;
}

/**
 * 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) {
  $filter_types = '';
  $questions = array();
  $wheres = array();
  $where_sql = '';
  if ($include_all_types === TRUE) {
    $types = array_keys(_quiz_get_question_types());
    if (count($types) > 0) {
      $str_types = "'" . implode("','", $types) . "'";
      $wheres[] = 'n.type IN (' . $str_types . ')';
    }
  }
  if (!is_null($quiz_vid)) {
    $wheres[] = 'qnr.parent_vid = ' . intval($quiz_vid);
    $wheres[] = 'qnr.parent_nid = ' . intval($quiz_nid);
  }

  // Only include published questions.
  $wheres[] = 'n.status = 1';

  // Create where filter to be added.
  if (count($wheres)) {
    $where_sql = ' WHERE ';
    $where_sql .= implode(' AND ', $wheres);
  }
  if ($include_random) {
    $on_filter = 'AND qnr.question_status != ' . QUESTION_NEVER;
  }
  else {
    $on_filter = 'AND qnr.question_status = ' . QUESTION_ALWAYS;
  }
  $result = db_query('SELECT n.nid, nr.vid, n.vid AS latest_vid, n.type, nr.title, nr.body, nr.format, qnr.question_status, qnr.weight, qnr.max_score
    FROM {node} n
    INNER JOIN {node_revisions} nr ON n.nid = nr.nid
    LEFT JOIN {quiz_node_relationship} qnr
    ON nr.vid = qnr.child_vid
    AND qnr.parent_vid = %d
    ' . $on_filter . '
    ' . $where_sql . ' ORDER BY weight', $quiz_vid);

  // 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->nid] = $n;
    }
  }
  return $questions;
}

/**
 * Get all results, or results for a given user and/or quiz.
 *
 * @param $nid
 *   Node ID for the quiz.
 * @param $uid
 *   Optional user ID to constrain results to just that user.
 *
 * @return
 *   A list of results.
 */
function _quiz_get_results($nid = '', $uid = 0) {
  $results = array();
  $args = array();
  $sql = "SELECT n.nid, n.title, u.name, u.uid, qnrs.result_id, qnrs.score, qnrs.time_start, qnrs.time_end, qnp.pass_rate\n          FROM {node} n\n          INNER JOIN {quiz_node_results} qnrs ON qnrs.nid = n.nid\n          INNER JOIN {users} u ON u.uid = qnrs.uid\n          INNER JOIN {quiz_node_properties} qnp ON qnrs.vid = qnp.vid\n          WHERE n.type = 'quiz'";
  if ($nid) {
    $sql .= " AND qnrs.nid = %d";
    $args[] = $nid;
  }
  if ($uid != 0) {
    $sql .= " AND qnrs.uid = %d";
    $args[] = $uid;
  }
  $sql .= " ORDER BY qnrs.result_id DESC";
  $dbresult = db_query(db_rewrite_sql($sql), $args);
  while ($line = db_fetch_array($dbresult)) {
    $results[$line['result_id']] = $line;
  }
  return $results;
}

/**
 * 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();
  $sql = "SELECT n.nid, n.vid, n.title, n.uid, u.name, n.created\n          FROM {node} n\n          LEFT JOIN {users} u\n          ON u.uid = n.uid\n          WHERE n.type = 'quiz'";
  if ($uid != 0) {
    $sql .= " AND n.uid = %d";
    $args[] = $uid;
  }
  $sql .= " ORDER BY n.nid DESC";
  $dbresult = db_query(db_rewrite_sql($sql), $args);
  while ($line = db_fetch_array($dbresult)) {
    $results[$line['nid']] = $line;
  }
  return $results;
}

/**
 * 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 = %d AND qt.tid = ra.tid)\n                   WHERE ra.result_id = %d\n                   ORDER BY ra.number, ra.answer_timestamp", $quiz->vid, $rid);
  while ($line = db_fetch_object($ids)) {

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

/**
 * 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) {
  $sql = "INSERT IGNORE INTO {quiz_node_results_answers}\n          (result_id, question_nid, question_vid, is_skipped, answer_timestamp, number";
  if ($quiz->randomization == 3) {
    $sql .= ', tid';
  }
  $sql .= ') VALUES ';
  $args = array();
  $first = TRUE;
  $time = time();
  foreach ($_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions'] as $previous_question) {
    if (!$first) {
      $sql .= ', ';
    }
    $sql .= '(%d, %d, %d, 1, ' . $time . ', %d';
    array_push($args, $rid, $previous_question['nid'], $previous_question['vid'], $previous_question['number']);
    if ($quiz->randomization == 3) {
      $sql .= ', %d';
      $args[] = $previous_question['tid'];
    }
    $sql .= ')';
    $first = FALSE;
  }
  db_query($sql, $args);
}

/**
 * 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 variable_get('quiz_name', 'Quiz');
}

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

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

/**
 * This function was copied from the triggers module as to prevent having to be
 * dependent on that module for the actions to work.  The trigger function is
 * called trigger_options().
 *
 * @param $type
 *   One of 'node', 'user', 'comment'.
 *
 * @return
 *   Array keyed by action ID.
 */
function quiz_action_options($type = 'all') {
  $options = array(
    t('Choose an action'),
  );
  foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) {
    $options[$action['type']][$aid] = $action['description'];
  }
  if ($type == 'all') {
    return $options;
  }
  else {
    $options[$type][0] = t('Choose an action');

    // Lets sort it to get the choose an action back first.
    ksort($options[$type]);
    return $options[$type];
  }
}

/**
 * Returns an array with quiz titles keyed with quiz node ids.
 *
 * @return array
 *   Options suitable for a form, in which the value is nid.
 */
function quiz_get_quiz_options() {
  $options = array();
  $results = db_query(db_rewrite_sql("SELECT n.nid, n.title, n.type FROM {node} n WHERE n.type = 'quiz'"));
  while ($result = db_fetch_object($results)) {
    $options[$result->nid] = drupal_substr(check_plain($result->title), 0, 30);
  }
  return $options;
}

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

/**
 * 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) {
  global $user;
  $new_node = new stdClass();
  $new_node->title = $title;
  $new_node->type = 'quiz';
  $new_node->status = 1;
  $additions = _quiz_get_node_defaults();
  foreach ($additions as $key => $value) {
    $new_node->{$key} = $value;
  }
  $new_node->quiz_open = _quiz_form_prepare_date();
  $new_node->quiz_close = _quiz_form_prepare_date(NULL, variable_get('quiz_default_close', 30));

  // Get default settings
  $settings_loaded = _quiz_load_user_settings($new_node);
  if (!$settings_loaded) {
    $new_node->def_uid = variable_get('quiz_def_uid', 1);
    _quiz_load_user_settings($new_node);
  }
  $new_node->auto_created = TRUE;
  $new_node->uid = $user->uid;
  node_save($new_node);
  if (is_numeric($new_node->nid)) {
    drupal_set_message(t('Quiz %title has been created.', array(
      '%title' => $title,
    )));
  }
  return $new_node;
}

/**
 * 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;
  }
  $node = menu_get_object();
  return $node->type == 'quiz' ? $node : FALSE;
}

/**
 * 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 (!isset($node->nid)) {
    return FALSE;
  }
  $sql = 'SELECT *
          FROM {quiz_node_results}
          WHERE nid = %d AND vid = %d
          LIMIT 1';
  $res = db_query($sql, $node->nid, $node->vid);
  return db_fetch_object($res) ? TRUE : FALSE;
}

/**
 * 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;
  }
  $sql = 'DELETE FROM {quiz_node_relationship}
          WHERE question_status = %d
          AND parent_vid = %d';
  db_query($sql, QUESTION_RANDOM, $node->vid);
}

/**
 * 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;
  }
  $sql = 'DELETE FROM {quiz_node_relationship}
          WHERE parent_vid = %d';
  db_query($sql, $node->vid);
}

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

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

  // Find original questions.
  $sql = 'SELECT child_nid, child_vid, question_status, weight, max_score
          FROM {quiz_node_relationship}
          WHERE parent_vid = %d';
  $res = db_query($sql, $node->translation_source->vid);
  while ($res_o = db_fetch_object($res)) {
    $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.
    node_save($original_question);

    // 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)
            VALUES(%d, %d, %d, %d, %d, %d, %d)';
    db_query($sql, $node->nid, $node->vid, $original_question->nid, $original_question->vid, $res_o->question_status, $res_o->weight, $res_o->max_score);
  }
}

/**
 * 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) {
  global $user;

  // Do not delete results for anonymous users
  if ($user->uid == 0) {
    return;
  }
  switch ($quiz->keep_results) {
    case QUIZ_KEEP_ALL:
      return FALSE;
    case QUIZ_KEEP_BEST:

      //I'm afraid of what this might do to performance...
      $sql = 'SELECT result_id
              FROM {quiz_node_results}
              WHERE nid = %d AND uid = %d AND is_evaluated = 1 AND result_id != (
                SELECT result_id
                FROM {quiz_node_results}
                WHERE score = (
                  SELECT MAX(score)
                  FROM {quiz_node_results}
                  WHERE nid = %d AND uid = %d
                )
                AND nid = %d AND uid = %d LIMIT 1
              )';
      $res = db_query($sql, $quiz->nid, $user->uid, $quiz->nid, $user->uid, $quiz->nid, $user->uid);
      $rids = array();
      while ($rid2 = db_result($res)) {
        $rids[] = $rid2;
      }
      quiz_delete_results($rids);
      return !empty($rids);
    case QUIZ_KEEP_LATEST:
      $sql = 'SELECT result_id
              FROM {quiz_node_results}
              WHERE nid = %d AND uid = %d AND is_evaluated = 1 AND result_id != %d';
      $res = db_query($sql, $quiz->nid, $user->uid, $rid);
      $rids = array();
      while ($rid2 = db_result($res)) {
        $rids[] = $rid2;
      }
      quiz_delete_results($rids);
      return !empty($rids);
  }
}

/**
 * Delete quiz results.
 *
 * @param $rids
 *   Result ids for the results to be deleted.
 */
function quiz_delete_results($rids) {

  // Security. We avoid db_placeholders to slightly improve performance.
  foreach ($rids as $key => $value) {
    $rids[$key] = intval($value);
  }
  if (empty($rids)) {
    return;
  }
  $rids_string = implode(', ', $rids);
  $sql = 'SELECT result_id, question_nid, question_vid
          FROM {quiz_node_results_answers}
          WHERE result_id IN(' . $rids_string . ')';
  $res = db_query($sql);
  while ($res_o = db_fetch_object($res)) {
    quiz_question_delete_result($res_o->result_id, $res_o->question_nid, $res_o->question_vid);
  }
  db_query('DELETE FROM {quiz_node_results_answers} WHERE result_id IN (' . $rids_string . ')');
  db_query('DELETE FROM {quiz_node_results} WHERE result_id IN (' . $rids_string . ')');
}

/**
 * Apply the users default settings to a quiz node.
 *
 * @param $node
 *   Quiz node.
 *
 * @return
 *   TRUE if success or FALSE if not success.
 */
function _quiz_load_user_settings(&$node) {
  global $user;
  $sql = "SELECT * FROM {quiz_user_settings} WHERE uid = %d";

  // The def_uid property is the default user id. It is used if there are no
  // settings store for the current user.
  $res = db_query($sql, isset($node->def_uid) ? $node->def_uid : $user->uid);
  if ($res_o = db_fetch_object($res)) {
    foreach ($res_o as $key => $value) {
      if (!in_array($key, array(
        'nid',
        'vid',
        'uid',
      ))) {
        $node->{$key} = $value;
      }
    }
    $result_options = db_query('SELECT * FROM {quiz_node_result_options} WHERE nid = %d AND vid= %d', $res_o->nid, $res_o->vid);
    while ($option = db_fetch_array($result_options)) {
      $node->resultoptions[] = $option;
    }
    return TRUE;
  }
  return FALSE;
}

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

  // We do not save settings if the node has been created by the system,
  // or if the user haven't requested it
  $node = (object) $node;
  if (isset($node->auto_created) || !isset($node->remember_settings) || !$node->remember_settings) {
    return FALSE;
  }

  // We're going to use drupal_write_record so we have to set the uid correctly first.
  // This is just temporary. We're not changing the nodes stored uid!
  $node->uid = isset($node->save_def_uid) ? $node->save_def_uid : $user->uid;

  // Are we updating or inserting?
  $sql = 'SELECT uid FROM {quiz_user_settings} WHERE uid = %d';
  $res = db_query($sql, $node->uid);
  $update = is_numeric(db_result($res)) ? array(
    'uid',
  ) : array();
  drupal_write_record('quiz_user_settings', $node, $update);
  drupal_set_message(t('Default settings have been saved'));
}

/**
 * Redirects to the manage questions tab if the quiz has no questions.
 *
 * @param $node
 *   Quiz node.
 */
function _quiz_redirect_if_empty($node) {
  $sql = 'SELECT child_vid
          FROM {quiz_node_relationship}
          WHERE parent_vid = %d';
  $res = db_query($sql, $node->vid);
  if (!($res_o = db_fetch_object($res))) {
    $_REQUEST['destination'] = 'node/' . $node->nid . '/questions';
  }
}

/**
 * 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) {
  $sql = 'SELECT count(*)
          FROM {quiz_node_relationship} qnr
          JOIN {node} n ON n.nid = qnr.child_nid
          WHERE n.status = 1 AND qnr.parent_vid = %d AND qnr.question_status = %d';
  return db_result(db_query($sql, $vid, QUESTION_ALWAYS));
}

/**
 * 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.
 * @param $format
 *   The format fieldset.
 */
function _quiz_format_mod(&$format) {
  $format['#attributes']['class'] = 'quiz-filter';
  if (isset($format['format'])) {
    $format['format']['guidelines']['#value'] = ' ';
    foreach ($format as $key => $value) {
      if (is_numeric($key)) {
        $format[$key]['#value'] = ' ';
      }
    }
  }
}

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

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

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

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

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

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

Functions

Namesort descending Description
quiz_access Implementation of hook_access().
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_action_options This function was copied from the triggers module as to prevent having to be dependent on that module for the actions to work. The trigger function is called trigger_options().
quiz_availability Find out if a quiz is available for taking or not
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_content_extra_fields Implementation of hook_content_extra_fields(cck)
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
quiz_delete Implementation of hook_delete().
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_enable
quiz_end_actions Actions to take at the end of a quiz
quiz_end_scoring Score a completed quiz.
quiz_form Implementation of hook_form().
quiz_form_alter Implementation of hook_form_alter().
quiz_get_all_titles Returns the titles for all quizzes the user has access to.
quiz_get_all_version_titles Returns the titles for all quizzes the user has access to.
quiz_get_number_of_questions Finds out the number of questions for the quiz.
quiz_get_pass_rate Finds out the pass rate 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_get_quiz_options Returns an array with quiz titles keyed with quiz node ids.
quiz_get_score_data Return highest score data for given quizzes.
quiz_has_been_answered Finds out if a quiz has been answered or not.
quiz_help Implementation of hook_help().
quiz_init Implementation of hook_init().
quiz_insert Implementation of hook_insert().
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_load Implementation of hook_load().
quiz_mail Implementation of hook_mail().
quiz_make_new Makes, saves and returns a new quiz node.
quiz_menu Implementation of hook_menu().
quiz_nodeapi Implementation of hook_nodeapi()
quiz_node_info Implementation of hook_node_info().
quiz_node_map Map node properties to a question object.
quiz_perm Implementation of hook_perm().
quiz_question_module_for_type
quiz_quiz_finished Implementation hook_quiz_finished().
quiz_quiz_scored Implementation hook_quiz_scored().
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 Primary quiz-taking view on 'Take' tab.
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 Implementation of hook_theme().
quiz_title_callback
quiz_type_access_load Load a quiz node, cache it and validate that it is indeed of type quiz.
quiz_type_confirm Validate that a node is of type quiz, and that the user has access to it.
quiz_update Implementation of hook_update().
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 Implementation of hook user().
quiz_validate Implementation of hook_validate().
quiz_view Implementation of hook_view().
quiz_views_api Implementation of hook_views_api().
_quiz_active_result_id Returns the result ID for any current result set for the given quiz.
_quiz_build_categorized_question_list Builds the questionlist for quizzes with categorized random questions
_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_results Get all results, or results for a given user and/or quiz.
_quiz_get_summary_text Get the summary message for a completed quiz.
_quiz_get_terms Get data for all terms belonging to a Quiz with categorized random questions
_quiz_get_vocabularies Retrieve list of vocabularies for all quiz question types.
_quiz_insert_resultoptions Insert call specific to result options.
_quiz_insert_result_option Insert one result option.
_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 Apply the users default settings to a quiz node.
_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_redirect_if_empty Redirects to the manage questions tab if the quiz has no questions.
_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_taxonomy_select Prints a taxonomy selection form for each vocabulary.
_quiz_update_resultoptions Modify result of option-specific updates.
_quiz_user_results_access Determine who should have access to the My results tab.

Constants