You are here

quiz.module in Quiz 6.2

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.
include drupal_get_path('module', 'quiz') . '/quiz_datetime.inc';

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

/**
 * Quiz perms.
 *
 * TODO: Simply adding the new quiz config perm for now - refactor other perms
 * to constants in the future.
 */
define('QUIZ_PERM_ADMIN_CONFIG', 'administer quiz configuration');

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

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

/**
 * Implementation of hook_perm().
 */
function quiz_perm() {
  return array(
    QUIZ_PERM_ADMIN_CONFIG,
    'access quiz',
    'create quiz',
    'administer quiz',
    'user results',
    'own results',
  );
}

/**
 * Implementation of hook_access().
 */
function quiz_access($op, $node, $account) {
  switch ($op) {
    case 'view':
      return user_access('access quiz');
    case 'create':
      return user_access('create quiz');
    case 'update':
    case 'delete':

      // This doesn't look right...
      return user_access('create quiz') && $user->uid == $node->uid;
    default:
      return user_access('administer quiz');
  }
}

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

  // MPB FIXME: 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_menu().
 */
function quiz_menu() {

  // ADMIN //
  $items['admin/settings/quiz'] = array(
    'title' => t('@quiz configuration', array(
      '@quiz' => QUIZ_NAME,
    )),
    'description' => t('Configure @quiz options.', array(
      '@quiz' => QUIZ_NAME,
    )),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quiz_admin_settings',
    ),
    'access arguments' => array(
      QUIZ_PERM_ADMIN_CONFIG,
    ),
    'type' => MENU_NORMAL_ITEM,
    // optional
    'file' => 'quiz.admin.inc',
  );
  $items['admin/quiz/results'] = array(
    'title' => t('@quiz results', array(
      '@quiz' => QUIZ_NAME,
    )),
    'page callback' => 'quiz_admin',
    'access arguments' => array(
      'administer quiz',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'quiz.admin.inc',
  );
  $items['admin/quiz/%/view'] = array(
    'title' => t('View @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
    'page callback' => 'quiz_admin_results',
    'page arguments' => array(
      2,
    ),
    'access arguments' => array(
      'administer quiz',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'quiz.admin.inc',
  );
  $items['admin/quiz/%/delete'] = array(
    'title' => t('Delete @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
    'page callback' => 'quiz_admin_result_delete',
    //'page arguments' => array(2),
    'access arguments' => array(
      'administer quiz',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'quiz.admin.inc',
  );

  // Menu item for adding questions to quiz.
  $items['node/%quiz_type_access/questions'] = array(
    'title' => t('Manage questions'),
    'page callback' => 'quiz_questions',
    'page arguments' => array(
      1,
    ),
    'access arguments' => array(
      'create quiz',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'quiz.admin.inc',
  );
  $items['node/%quiz_type_access/admin'] = array(
    'title' => t('Quiz admin', array(
      '@quiz' => QUIZ_NAME,
    )),
    'page callback' => 'theme',
    'page arguments' => array(
      'quiz_view',
      1,
    ),
    'access arguments' => array(
      'administer quiz',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'quiz.admin.inc',
  );

  // USER //
  $items['user/%/myresults'] = array(
    'title' => t('My results'),
    'page callback' => 'quiz_get_user_results',
    'page arguments' => array(
      1,
    ),
    'access arguments' => array(
      'own results',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'quiz.pages.inc',
  );
  $items['user/quiz/%/userresults'] = array(
    'title' => t('User results'),
    'page callback' => 'quiz_user_results',
    'page arguments' => array(
      2,
    ),
    'access arguments' => array(
      'own results',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'quiz.pages.inc',
  );
  return $items;
}

/**
 * Implementation of hook_theme().
 */
function quiz_theme() {
  return array(
    'quiz_availability' => array(
      'arguments' => array(
        'node' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_view' => array(
      'arguments' => array(
        'node' => NULL,
        'teaser' => FALSE,
        'page' => FALSE,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_get_user_results' => array(
      'arguments' => array(
        'results' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_question_table' => array(
      'arguments' => array(
        'questions' => NULL,
        'quiz_id' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_filtered_questions' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_take_question' => array(
      'arguments' => array(
        'quiz' => NULL,
        'question_node' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_take_summary' => array(
      'arguments' => array(
        'quiz' => NULL,
        'questions' => NULL,
        'score' => 0,
        'summary' => '',
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_admin' => array(
      'arguments' => array(
        'results' => NULL,
      ),
      'file' => 'quiz.admin.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_feedback' => array(
      'arguments' => array(
        'questions' => NULL,
        'showpoints' => TRUE,
        'showfeedback' => FALSE,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_question_feedback' => array(
      'arguments' => array(
        'quiz' => NULL,
        'report' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_questions' => array(
      'arguments' => array(
        'form' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_progress' => array(
      'arguments' => array(
        'question_number' => NULL,
        'num_of_question' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_question_table' => array(
      'arguments' => array(
        'questions' => NULL,
        'quiz_id' => NULL,
      ),
      'file' => 'quiz.pages.inc',
    ),
    'quiz_no_feedback' => array(
      'file' => 'quiz.pages.inc',
      'arguments' => array(),
    ),
  );
}

/**
 * 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) {
  if ($form_id == 'quiz_node_form') {

    // Remove preview button:
    unset($form['buttons']['preview']);
  }
}

/**
 * Implementation of hook_insert().
 */
function quiz_insert($node) {
  quiz_translate_form_date($node, 'quiz_open');
  quiz_translate_form_date($node, 'quiz_close');
  $sql = "INSERT INTO {quiz_node_properties} \n    (vid, nid, number_of_random_questions, shuffle, \n      backwards_navigation, quiz_open, quiz_close, takes, pass_rate, summary_pass, \n      summary_default, quiz_always, feedback_time, tid) \n    VALUES(%d, %d, %d, %d, %d, %d, %d, %d, %d, '%s', '%s', %d, %d, %d)";
  db_query($sql, $node->vid, $node->nid, $node->number_of_random_questions, $node->shuffle, $node->backwards_navigation, $node->quiz_open, $node->quiz_close, $node->takes, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always, $node->feedback_time, $node->tid);
  _quiz_insert_resultoptions($node);
}

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

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

    // Insert a new row in the quiz_node_properties table.
    quiz_insert($node);

    // 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_translate_form_date($node, 'quiz_open');
    quiz_translate_form_date($node, 'quiz_close');
    $sql = "UPDATE {quiz_node_properties} \n      SET vid = %d,\n        shuffle = %d,\n        backwards_navigation = %d,\n        quiz_open = %d,\n        quiz_close = %d,\n        takes = %d,\n        pass_rate = %d,\n        summary_pass = '%s',\n        summary_default = '%s',\n        quiz_always = %d,\n        feedback_time = %d,\n        number_of_random_questions = %d\n      WHERE vid = %d \n        AND nid = %d";
    db_query($sql, $node->vid, $node->shuffle, $node->backwards_navigation, $node->quiz_open, $node->quiz_close, $node->takes, $node->pass_rate, $node->summary_pass, $node->summary_default, $node->quiz_always, $node->feedback_time, $node->number_of_random_questions, $node->vid, $node->nid);
  }
  _quiz_update_resultoptions($node);
}

/**
 * Implementation of hook_delete().
 */
function quiz_delete($node) {

  // This first line should load all the vid's for the nid.
  db_query('DELETE FROM {quiz_node_properties} WHERE vid = %d AND nid = %d', $node->vid, $node->nid);
  db_query('DELETE FROM {quiz_node_relationship} WHERE parent_nid = %d', $node->nid);
  db_query('DELETE FROM {quiz_node_results} WHERE vid = %d AND nid = %d', $node->vid, $node->nid);
  db_query('DELETE FROM {quiz_node_result_options} WHERE vid = %d AND nid = %d', $node->vid, $node->nid);
}

/**
 * Implementation of hook_load().
 */
function quiz_load($node) {
  $quiz_vid = $node->vid;
  $additions = db_fetch_object(db_query('SELECT qnp.* FROM {quiz_node_properties} qnp WHERE qnp.vid = %d AND qnp.nid = %d ORDER BY qnp.property_id DESC', $quiz_vid, $node->nid));

  /* Removed because it is non-functional. There is no 4question->child_nid, nor is their a $question->status.
    $results   = db_query('SELECT nr.nid, qnr.question_status
      FROM {quiz_node_relationship} qnr
      INNER JOIN {node_revisions} nr ON (qnr.parent_vid = nr.vid AND qnr.parent_nid = nr.nid)
      WHERE qnr.parent_vid = %d AND qnr.parent_nid = %d', $quiz_vid, $node->nid);

    while ($question = db_fetch_object($results)) {
      $additions->status[$question->child_nid] = $question->status;
    }
    */
  $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) {
  if (!$teaser) {

    //load the questions in the view page
    $node->content['body']['#value'] = quiz_take_quiz($node);
  }
  else {
    $node = node_prepare($node, $teaser);
  }
  return $node;
}

// QUIZ FORM

/**
 * 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['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'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => $node->body,
    '#description' => t('A description of what the @quiz entails', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#required' => TRUE,
  );
  $form['body_filter']['format'] = filter_form($node->format);
  $form['shuffle'] = array(
    '#type' => 'checkbox',
    '#title' => t('Shuffle questions'),
    '#default_value' => isset($node->shuffle) ? $node->shuffle : 1,
    '#description' => t('Whether to shuffle/randomize the questions on the @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
  );
  $form['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['feedback_time'] = array(
    '#title' => t('Feedback Time'),
    '#type' => 'radios',
    '#default_value' => isset($node->feedback_time) ? $node->feedback_time : QUIZ_FEEDBACK_END,
    '#options' => _quiz_get_feedback_options(),
    '#description' => t('Indicates at what point feedback for each question will be given to the user'),
  );

  // Set up the availability options.
  $form['quiz_availability'] = array(
    '#type' => 'fieldset',
    '#title' => t('Availability options'),
    '#collapsed' => FALSE,
    '#collapsible' => TRUE,
  );
  $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,
    )),
  );
  $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,
    )),
  );
  $options = array(
    t('Unlimited'),
  );
  for ($i = 1; $i < 10; $i++) {
    $options[$i] = $i;
  }
  $form['takes'] = array(
    '#type' => 'select',
    '#title' => t('Number of takes'),
    '#default_value' => $node->takes,
    '#options' => $options,
    '#description' => t('The number of times a user is allowed to take the @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
  );

  // Quiz summary options.
  $form['summaryoptions'] = array(
    '#type' => 'fieldset',
    '#title' => t('@quiz Summary Options', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );

  // If pass/fail option is checked, present the form elements.
  if (variable_get('quiz_use_passfail', 1)) {

    // New nodes get the default.
    if (!$node->nid) {
      $node->pass_rate = variable_get('quiz_default_pass_rate', 75);
    }
    $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. (For personality quiz enter 0, and use result options.)', 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,
      )),
    );
  }
  else {
    $form['summaryoptions']['pass_rate'] = array(
      '#type' => 'hidden',
      '#value' => variable_get('quiz_default_pass_rate', 75),
      '#required' => FALSE,
    );
  }
  $form['summaryoptions']['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."),
  );
  $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,
  );
  $form['resultoptions'] = array(
    '#type' => 'fieldset',
    '#title' => t('@quiz Results', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#tree' => TRUE,
  );
  $options = $node->resultoptions;
  $num_options = max(3, !empty($options) ? count($options) : 5);
  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'),
      '#description' => t('Not displayed on personality @quiz.', array(
        '@quiz' => QUIZ_NAME,
      )),
      '#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). Leave blank for personality quizzes.'),
      '#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). Leave blank for personality quizzes.'),
      '#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.'),
    );
    if ($option['option_id']) {
      $form['resultoptions'][$i]['option_id'] = array(
        '#type' => 'hidden',
        '#value' => $option['option_id'],
      );
    }
  }
  return $form;
}

/**
 * Implementation of hook_validate().
 */
function quiz_validate($node) {
  if (!$node->nid && empty($_POST)) {
    return;
  }
  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 (!is_numeric($node->pass_rate)) {
    form_set_error('pass_rate', t('The pass rate value must be a number between 0% and 100%.'));
  }
  if ($node->pass_rate > 100) {
    form_set_error('pass_rate', t('The pass rate value must not be more than 100%.'));
  }
  if ($node->pass_rate < 0) {
    form_set_error('pass_rate', t('The pass rate value must not be less than 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 (!is_numeric($option[$bound])) {
            form_set_error($bound, t('The range %start value must be a number between 0% and 100%.', array(
              '%start' => $bound_text,
            )));
          }
          if ($option[$bound] < 0) {
            form_set_error($bound, t('The range %start value must not be less than 0%.', array(
              '%start' => $bound_text,
            )));
          }
          if ($option[$bound] > 100) {
            form_set_error($bound, t('The range %start value must not be more than 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);
        }
      }
    }
    else {
      if (!empty($option['option_summary'])) {
        form_set_error('option_summary', t('Option has a summary, but no name.'));
      }
    }
  }
  if ($node->pass_rate == 0 && !$num_options) {
    form_set_error('pass_rate', t('Unscored quiz, but no result options defined.'));
  }
}

// END HOOKS

/**
 * Load a quiz node and validate it.
 *
 * @param $arg 
 *  The Node ID
 * @return 
 *  A quiz node object or FALSE if a load failed.
 */
function quiz_type_access_load($arg) {

  // Simple verification/load of the node.
  return ($node = node_load($arg)) && $node->type == 'quiz' ? $node : FALSE;
}

/**
 * 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 integer
 *  Returns the number of quiz questions.
 */
function quiz_get_number_of_questions($vid, $nid) {
  $sql = 'SELECT COUNT(*) + (SELECT number_of_random_questions FROM {quiz_node_properties} WHERE vid = %d AND nid = %d)
    FROM {quiz_node_relationship} qnr
    WHERE qnr.parent_vid = %d
      AND qnr.parent_nid = %d
      AND question_status = %d';
  return db_result(db_query($sql, $vid, $nid, $vid, $nid, QUESTION_ALWAYS));
}

/**
 * Finds out the pass rate for the quiz.
 *
 * @param $nid
 *  The quiz ID.
 * @return integer
 *  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));
}

/**
 * Updates quiz-question relation entries in the quiz_node_relationship table.
 *
 * @access public
 * @param integer $old_quiz_vid
 *  The quiz vid prior to a new revision.
 * @param integer $new_quiz_vid
 *  The quiz vid of the latest revision.
 */
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) \n    SELECT src.parent_nid, %d, src.child_nid, src.child_vid, src.question_status \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);
}

/**
 * Handles quiz taking.
 *
 * This gets executed when the main quiz node is first loaded.
 *
 * @param $quiz
 *  The quiz node.
 * 
 * @return
 *  HTML output for page.
 */
function quiz_take_quiz($quiz) {

  // If no access, fail.
  if (!user_access('access quiz')) {
    drupal_access_denied();
    return;
  }
  if (!isset($quiz)) {
    drupal_not_found();
    return;
  }

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

  /* Moved to quiz_start_actions()
    if ($user->uid && $quiz->takes != 0) {
      $query = "SELECT COUNT(result_id) FROM {quiz_node_results} WHERE nid = %d AND vid = %d AND uid = %d";
      $times = db_result(db_query($query, $quiz->nid, $quiz->vid, $user->uid));
      if ($times >= $quiz->takes) {
        drupal_set_message(t('You have already taken this @quiz %d times.', array('@quiz' => QUIZ_NAME, '%d' => $times)), 'status');
        return;
      }
    }
    */
  if (!isset($_SESSION['quiz_' . $quiz->nid]['quiz_questions'])) {

    // First time running through quiz.
    if ($rid = quiz_start_actions($quiz)) {

      // Create question list.
      $questions = quiz_build_question_list($quiz);
      if ($questions === FALSE) {
        drupal_set_message(t('Not enough random questions were found. Please !add more questions before trying to take this @quiz.', array(
          '@quiz' => QUIZ_NAME,
          '!add more questions' => l('add more questions', 'node/' . arg(1) . '/questions'),
        )), 'error');
        return '';
      }
      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('assign questions', 'node/' . arg(1) . '/questions'),
        )), 'error');
        return '';
      }

      // Initialize session variables.
      $_SESSION['quiz_' . $quiz->nid]['quiz_questions'] = $questions;
      $_SESSION['quiz_' . $quiz->nid]['result_id'] = $rid;
      $_SESSION['quiz_' . $quiz->nid]['question_number'] = 0;
    }
    else {
      return '';
    }
  }

  // Navigate backwards
  if ($_POST['op'] == t('Back')) {
    unset($_POST['tries']);

    // We maintain two lists -- previous questions and upcomming questions.
    // When we go backward, we pop one from the previous and prepend it to
    // the upcomming.
    // TODO: This can be maintained more efficiently with a single array of
    // all questions and then a pointer to the current question. That makes
    // rewinding much easier.
    $quiz_id = 'quiz_' . $quiz->nid;
    $last_q = array_pop($_SESSION[$quiz_id]['previous_quiz_questions']);
    array_unshift($_SESSION[$quiz_id]['quiz_questions'], $last_q);
  }

  // Check for answer submission.
  if ($_POST['op'] == t('Submit')) {
    if (!isset($_POST['tries'])) {
      drupal_set_message(t('You must select an answer before you can progress to the next question!'), 'error');
    }
    else {

      //unset($_SESSION['quiz_'. $quiz->nid]['previous_quiz_questions']);

      // 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]['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(array(
        'nid' => $former_question_array['nid'],
      ));

      // Call hook_evaluate_question
      $result = module_invoke($former_question->type, 'evaluate_question', $former_question, $_SESSION['quiz_' . $quiz->nid]['result_id']);
      quiz_store_question_result($former_question_array['nid'], $former_question_array['vid'], $_SESSION['quiz_' . $quiz->nid]['result_id'], $result);

      // Stash feedback in the session, since the $_POST gets cleared.
      if ($quiz->feedback_time == QUIZ_FEEDBACK_QUESTION) {
        $report = module_invoke($former_question->type, 'get_report', $former_question_array['nid'], $former_question_array['vid'], $_SESSION['quiz_' . $quiz->nid]['result_id']);
        $_SESSION['quiz_' . $quiz->nid]['feedback'] = rawurlencode(quiz_get_feedback($quiz, $report));
      }

      // If anonymous user, refresh url with unique hash to prevent caching.
      if (!$user->uid) {
        drupal_goto('node/' . $quiz->nid . '/quiz/start/' . md5(mt_rand() . time()));
      }
    }
  }

  // If we had feedback from the last question.
  if (isset($_SESSION['quiz_' . $quiz->nid]['feedback']) && $quiz->feedback_time == QUIZ_FEEDBACK_QUESTION) {
    $output .= rawurldecode($_SESSION['quiz_' . $quiz->nid]['feedback']);
  }

  // If this quiz is in progress, load the next questions and return it via the theme.
  if (!empty($_SESSION['quiz_' . $quiz->nid]['quiz_questions'])) {
    $question_node = node_load(array(
      'nid' => $_SESSION['quiz_' . $quiz->nid]['quiz_questions'][0]['nid'],
    ));
    $output .= theme('quiz_take_question', $quiz, $question_node);
    unset($_SESSION['quiz_' . $quiz->nid]['feedback']);
  }
  else {
    $score = quiz_end_actions($quiz, $_SESSION['quiz_' . $quiz->nid]['result_id']);
    if ($quiz->feedback_time == QUIZ_FEEDBACK_NEVER) {
      $output = theme('quiz_no_feedback');
    }
    else {

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

      // Get the themed summary page.
      $output .= theme('quiz_take_summary', $quiz, $questions, $score, $summary);
    }

    // Remove session variables.
    unset($_SESSION['quiz_' . $quiz->nid]);
  }
  return $output;
}

/**
 * Store a quiz question result.
 */
function quiz_store_question_result($nid, $vid, $rid, $is_correct) {

  //watchdog('quiz', 'quiz_store_question_result: storing.');
  $result = db_result(db_query("SELECT COUNT('result_id') AS count FROM {quiz_node_results_answers} WHERE question_nid = %d AND question_vid = %d AND result_id = %d", $nid, $vid, $rid));
  if ($result && $result['count'] > 0) {

    //watchdog('quiz', 'quiz_store_question_result: updating');
    db_query("UPDATE {quiz_node_results_answers} " . "SET is_correct = %d, points_awarded = %d, answer_timestamp = %d " . "WHERE question_nid = %d AND question_vid = %d AND result_id = %d", $is_correct, $is_correct ? 1 : 0, time(), $nid, $vid, $rid);
  }
  else {

    //watchdog('quiz', 'quiz_store_question_result: inserting');
    db_query("INSERT INTO {quiz_node_results_answers} " . "(question_nid, question_vid, result_id, is_correct, points_awarded, answer_timestamp) " . "VALUES (%d, %d, %d, %d, %d, %d)", $nid, $vid, $rid, $is_correct, $is_correct ? 1 : 0, time());
  }
}

/**
 * Actions to take at the end of a quiz.
 */
function quiz_end_actions($quiz, $rid) {
  $score = quiz_calculate_score($quiz, $rid);
  db_query("UPDATE {quiz_node_results} SET time_end = %d, score = %d WHERE result_id = %d", time(), $score['percentage_score'], $_SESSION['quiz_' . $quiz->nid]['result_id']);
  return $score;
}

/**
 * Get feedback for one question.
 *
 * Good for displaying feedback after every question instead of all at the end.
 *
 * @param $quiz
 *  The quiz node.
 * @param $report
 *  The question node and its calculated results.
 * @return
 *  Themed feedback for output.
 */
function quiz_get_feedback($quiz, $report) {
  return theme('quiz_question_feedback', $quiz, $report);
}

/**
 * Get answers the user actually tried.
 *
 * @param $answers
 *  The question answers.
 * @param $tried
 *  The user selected answers.
 * @return
 *  An array of user-selected answer keys.
 */
function quiz_get_answers($answers, $tried) {
  $selected = array();
  if (is_array($answers)) {
    foreach ($answers as $key => $answer) {
      if (($key = array_search($answer['aid'], $tried)) !== FALSE) {
        $selected[] = $answer;

        // Correct answer was selected, so lets take that out the tried list.
        unset($tried[$key]);
      }
    }
  }
  return $selected;
}

/**
 * Get an array of correct answer(s) for a question.
 *
 * @param $answers
 *  An array of question answers.
 * @return
 *  An array of correct answers.
 */
function quiz_get_corrects($answers) {
  if (is_array($answers)) {
    foreach ($answers as $answer) {
      if ($answer['is_correct'] > 0) {
        $corrects[] = $answer;
      }
    }
  }
  return $corrects;
}

/**
 * Check a user/quiz combo to see if the user passed the given quiz.
 * A quiz is keyed by nid/vid, so you need both.
 * 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.
 * @return
 *  Returns quiz_node_results result_id, or FALSE if there is an error.
 */
function quiz_start_actions($quiz) {
  global $user;
  $user_is_admin = user_access('create quiz');

  // 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) {
      drupal_set_message(t('This @quiz is not currently available.', array(
        '@quiz' => QUIZ_NAME,
      )), 'status');
      if (!$user_is_admin) {

        // 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 = %s AND nid = %s AND vid = %s";
    $taken = db_result(db_query($query, $user->uid, $quiz->nid, $quiz->vid));
    $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 (nid/vid combo).
    if ($taken) {

      // If the user has already taken this quiz too many times, stop the user.
      if ($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');
        if (!$user_is_admin) {
          return FALSE;
        }
      }
      else {
        if (variable_get('quiz_show_allowed_times', TRUE)) {
          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 (a) user is registered, and (b) 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');
  }

  // On error, we want to return before here to avoid creating an empty entry in quiz_node_results.
  // Otherwise, we get fairly clutter Quiz Results.
  // Insert quiz_node_results record.
  $result = db_query("INSERT INTO {quiz_node_results} (result_id, nid, vid, uid, time_start) VALUES (%d, %d, %d, %d, %d)", $rid, $quiz->nid, $quiz->vid, $user->uid, time());
  if ($result) {

    // Return the last RID.
    return 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;
  }
}

/**
 * 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) {
  if ($quiz->pass_rate > 0) {
    $score = db_fetch_array(db_query("SELECT count(*) as question_count, sum(is_correct) as num_correct " . "FROM {quiz_node_results_answers} " . "WHERE result_id = %d", $rid));
    if ($score['question_count'] > 0) {
      $score['percentage_score'] = round($score['num_correct'] * 100 / $score['question_count']);
    }
  }
  else {
    $score = db_fetch_array(db_query("SELECT " . "(SELECT count(*) FROM {quiz_node_results_answers} WHERE result_id = %d) as question_count," . "(SELECT option_summary " . "FROM {quiz_multichoice_user_answers} " . "LEFT JOIN {quiz_multichoice_answers} USING (answer_id) " . "LEFT JOIN {quiz_node_result_options} ON (result_option = option_id) " . "WHERE result_id = %d " . "GROUP BY result_option ORDER BY COUNT(result_option) desc LIMIT 1) as result_option", $rid, $rid));
  }
  return $score;
}

/**
 * Retrieves a question list for a given quiz.
 *
 * @param $quiz
 *  Quiz node.
 * @return
 *  Array of question node IDs.
 */
function quiz_build_question_list($quiz) {
  $questions = array();

  // Get required questions first.
  $result = db_query("SELECT child_nid as nid, child_vid as vid FROM {quiz_node_relationship} WHERE parent_vid = %d AND parent_nid = %d AND question_status = %d", $quiz->vid, $quiz->nid, 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) {

    //$questions = array_merge($questions, _quiz_get_random_questions($quiz->number_of_random_questions, $quiz->tid));
    $questions = array_merge($questions, _quiz_get_random_questions($quiz));
    if ($quiz->number_of_random_questions > count($questions)) {

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

  // Shuffle questions if required.
  if ($quiz->shuffle == 1) {
    shuffle($questions);
  }
  return $questions;
}

/**
 * Gets the number questions of a given type for a quiz.
 * 
 * @param $nid
 *  Node ID of the quiz.
 * @param $type
 *  Status constant.
 * @return
 *  Number of questions that meet the criteria.
 */
function quiz_get_num_questions($nid, $vid, $type) {
  return db_result(db_query("SELECT COUNT('parent_vid') FROM {quiz_node_relationship} WHERE parent_vid = %d AND parent_nid = %d AND question_status = %d", $vid, $nid, $type));
}

/**
 * Map node properties to a question object.
 *
 * @param $node
 *  Node
 * @return
 *  Question object
 */
function quiz_node_map($node) {
  $new_question = new stdClass();
  $new_question->question = check_markup($node->body, $node->format);
  $new_question->nid = $node->nid;
  $new_question->vid = $node->vid;
  $new_question->type = $node->type;
  $new_question->status = isset($node->question_status) ? $node->question_status : QUESTION_NEVER;
  return $new_question;
}

/**
 * Updates the status of questions assigned to the quiz. Possible statuses
 * are 'random', 'always', 'never'.
 *
 * @access public
 * @param &$quiz
 *  The quiz node. This is modified internally.
 * @param $submitted_questions
 *  Array of submitted question statuses indexed (keyed) by the question nid.
 * @return boolean
 *  True if update was a success, false if there was a problem.
 */
function quiz_update_questions(&$quiz, $submitted_questions, $revision = FALSE) {

  // No questions to update, so return now.
  if (empty($submitted_questions)) {
    return FALSE;

    // This will cause an error message.
  }

  // Loop through all questions and determine whether an update needs to be made.
  // As we go, we store the questions that will need to be updated.
  $existing_questions = _quiz_get_questions($quiz->vid, TRUE, TRUE);
  $i_am_different_now = FALSE;
  $questions = array();

  // These are the questions that will be put into the Quiz.
  foreach ($submitted_questions as $nid => $stat) {
    $existing = $existing_questions[$nid];
    if ($stat != QUESTION_NEVER) {
      if ($existing) {
        if ($existing->status != $stat) {

          // Question's status has been changed.
          $existing->status = $stat;
          $i_am_different = TRUE;
        }

        // else This question is the same. Do nothing.
        $questions[] = $existing;
      }
      else {
        $new_question = node_load($nid);
        $new_question->status = $stat;
        $questions[] = $new_question;

        // A new question was added to the quiz.
        $i_am_different = TRUE;
      }
    }
    elseif (!empty($existing) && $stat != $existing->status) {

      // Delete items moved from ALWAYS or RANDOM to NEVER
      $existing->status = $stat;
      $questions[] = $existing;
      $i_am_different = TRUE;
    }

    // else Question isn't on the quiz, and isn't marked for inclusion.

    //else {

    // Question isn't part of the quiz.

    //}
  }
  if (!$i_am_different) {

    // Nothing else to do.
    return TRUE;
  }

  // If we get here, then we (may) need to create a new VID and then store the questions.
  if ($revision) {

    // Create a new Quiz VID
    $quiz->revision = 1;

    // Need new vid.
    node_save($quiz);

    // This updates the $quiz referent.
  }

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

  // Now we do an insert of everything in the quiz.
  $sql = "INSERT INTO {quiz_node_relationship} (parent_nid, parent_vid, child_nid, child_vid, question_status) \n      VALUES (%d, %d, %d, %d, %d)";
  foreach ($questions as $question) {
    if ($question->status != QUESTION_NEVER) {

      //drupal_set_message(t("Doing insert for %nid-%vid-%cnid-%cvid: %stat", array('%nid' => $quiz->nid, '%vid' => $quiz->vid, '%cnid' => $question->nid, '%cvid' => $question->vid, '%stat' => $question->status)));
      $result = db_query($sql, $quiz->nid, $quiz->vid, $question->nid, $question->vid, $question->status);
    }
  }
  return TRUE;
}

// End of "Public" Functions

/**
 * Insert call specific to result options.
 *
 * This is called by quiz_insert().
 *
 * @param $node
 *  The quiz node.
 */
function _quiz_insert_resultoptions($node) {
  if (!$node->resultoptions) {
    return;
  }
  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) {
  if (!$node->resultoptions) {
    return;
  }
  foreach ($node->resultoptions as $option) {
    if (!empty($option['option_name']) && empty($option['option_id'])) {

      // Oops, this is actually a new result option.
      $option['nid'] = $node->nid;
      $option['vid'] = $node->vid;

      // ...so insert it.
      _quiz_insert_result_option($option);
    }
    else {

      // Update an existing result option.
      $sql = "UPDATE {quiz_node_result_options} \n        SET option_name='%s', option_summary='%s', option_start = %d,  option_end = %d \n        WHERE nid=%d AND vid=%d AND option_id=%d";
      $values = array(
        $option['option_name'],
        $option['option_summary'],
        $option['option_start'],
        $option['option_end'],
        $node->nid,
        $node->vid,
        $option['option_id'],
      );
      db_query($sql, $values);
    }
  }
}

/**
 * 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 
 * whether this is being called from admin/quiz/[quizid]/view.
 * 
 * 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) {
  if ($score['result_option']) {

    // Unscored quiz, return the proper result option.
    return $score['result_option'];
  }
  $admin = arg(3) == 'view';
  if ($quiz->pass_rate > 0) {
    $score_result = _quiz_pick_result_option($quiz->nid, $quiz->vid, $score['percentage_score']);
  }

  // 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 = t('The user passed this quiz.');
    }
    else {
      if (trim($quiz->summary_pass) != '') {
        $summary = !empty($score_result) ? $score_result : 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 = t('The user failed this quiz.');
      }
      else {
        $summary = t('the user completed this quiz.');
      }
    }
    else {
      if (trim($quiz->summary_default) != '') {
        $summary = !empty($score_result) ? $score_result : 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));
}
function _quiz_get_random_questions($quiz) {
  if (!is_object($quiz)) {
    drupal_set_message('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);

      /*
      // 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
        FROM {node} n
        INNER JOIN {term_node} tn USING (nid)
        WHERE n.status = 1 AND tn.tid IN ($term_ids)
        AND n.type IN ('"
        . implode("','", _quiz_get_question_types())
        . "') ORDER BY RAND()", 0, $num_random);
      */
    }
    else {

      // Select random question from assigned pool.
      $result = db_query_range("SELECT child_nid as nid, child_vid as vid FROM {quiz_node_relationship} WHERE parent_vid = %d AND parent_nid = %d AND question_status = %d ORDER BY RAND()", $quiz->vid, $quiz->nid, QUESTION_RANDOM, 0, $quiz->number_of_random_questions);
      while ($question_node = db_fetch_array($result)) {
        $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("','", _quiz_get_question_types()) . "') ORDER BY RAND()", 0, $num_random);
  $questions = array();
  while ($question_node = db_fetch_array($result)) {
    $questions[] = $question_node;
  }
  return $questions;
}

/**
 * Retrieve list of question types.
 *
 * Determined by which modules implement the list_questions() hook.
 *
 * @return
 *  Array of question types.
 */
function _quiz_get_question_types() {
  return module_implements('list_questions');
}

/**
 * Retrieve list of vocabularies for all quiz question types.
 *
 * @return
 *  An array containing a vocabulary list.
 */
function _quiz_get_vocabularies() {
  $vocabularies = array();
  foreach (_quiz_get_question_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.
 *
 * @return
 *  An array of questions.
 */
function _quiz_get_questions($quiz_vid = NULL, $include_all_types = TRUE, $nid_keys = FALSE) {
  $quiz = node_load((int) arg(1));
  $filter_types = '';
  $questions = array();
  $where_add = array();
  $where_sql = '';
  if ($include_all_types === TRUE) {
    $types = _quiz_get_question_types();
    if (count($types)) {
      $str_types = "'" . implode("','", $types) . "'";
      $where_add[] = 'question.type IN ( ' . $str_types . ' )';
    }
  }
  if (!is_null($quiz_vid)) {
    $where_add[] = 'qnr.parent_vid = ' . (int) $quiz_vid;
    $where_add[] = 'qnr.parent_nid = ' . $quiz->nid;
  }

  // Only include published questions.
  $where_add[] = 'question.status = 1';
  if (count($where_add)) {
    $where_sql = ' WHERE ';
    foreach ($where_add as $where) {
      $where_sql .= $where . ' AND ';
    }
    $where_sql = trim($where_sql, ' AND ');
  }
  $result = db_query('SELECT DISTINCT question.nid, question.vid, question.type, nr.body, nr.format, qnr.question_status
    FROM {node} question
    INNER JOIN {node_revisions} nr ON question.nid = nr.nid
    LEFT JOIN {quiz_node_relationship} qnr ON nr.vid = qnr.child_vid
      AND qnr.parent_vid = %d
      AND qnr.question_status != %d
    ' . $where_sql, $quiz_vid, QUESTION_NEVER);

  // Create questions array.
  if ($nid_keys === FALSE) {
    while ($node = db_fetch_object($result)) {
      $questions[] = quiz_node_map($node);
    }
  }
  else {
    while ($node = db_fetch_object($result)) {
      $n = quiz_node_map($node);
      $questions[$n->nid] = $n;
    }
  }
  return $questions;
}

// quiz_questions_form

/**
 * Retrieve list of published questions not assigned to quiz.
 *
 * @access public
 * @param integer $quiz_nid
 * @return array
 *  Array of questions objects.
 */
function _quiz_get_unused_questions($quiz_vid = NULL, $nid_keys = FALSE) {
  $quiz = menu_get_object();
  $types = _quiz_get_question_types();
  $where_sql = '';
  $questions = array();
  if (count($types)) {
    $where_sql = "AND question.type IN ('" . implode("','", $types) . "')";
  }
  $result = db_query('SELECT DISTINCT question.nid, question.vid, question.type, nr.body, nr.format
    FROM {node} question
    LEFT JOIN {node_revisions} nr ON (question.nid = nr.nid)
    WHERE question.status = 1
    AND (question.vid NOT IN
      (SELECT DISTINCT qnr.child_vid
      FROM {quiz_node_relationship} qnr
      WHERE qnr.parent_vid = %d
      AND qnr.question_status != ' . QUESTION_NEVER . '))
    ' . $where_sql, $quiz_vid);

  // Create questions array.
  if ($nid_keys === FALSE) {
    while ($node = db_fetch_object($result)) {
      $questions[] = quiz_node_map($node);
    }
  }
  else {
    while ($node = db_fetch_object($result)) {
      $n = quiz_node_map($node);
      $questions[$n->nid] = $n;
    }
  }
  return $questions;
}

/*
 * Get a full results list.
 */
function _quiz_get_results($nid = '', $uid = 0) {
  $results = array();
  $args = array();
  $sql = "SELECT n.nid, n.title, u.name, qnrs.result_id, qnrs.time_start, qnrs.time_end\n          FROM {node} n\n          INNER JOIN {quiz_node_properties} qnp\n          INNER JOIN {quiz_node_results} qnrs\n          INNER JOIN {users} u\n          WHERE n.type = 'quiz'\n            AND n.nid = qnp.nid\n            AND qnrs.nid = qnp.nid\n            AND u.uid = qnrs.uid";
  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 ASC";
  $dbresult = db_query($sql, $args);
  while ($line = db_fetch_array($dbresult)) {
    $results[$line['result_id']] = $line;
  }
  return $results;
}
function _quiz_get_answers($rid) {
  $questions = array();
  $ids = db_query("SELECT question_nid, question_vid, type \n    FROM {quiz_node_results_answers} \n    LEFT JOIN {node} ON (question_nid = nid AND question_vid = vid) \n    WHERE result_id = %d \n    ORDER BY answer_timestamp", $rid);
  while ($line = db_fetch_object($ids)) {

    //watchdog('quiz','_quiz_get_answers: looping through question');
    $questions[$line->question_nid] = module_invoke($line->type, 'get_report', $line->question_nid, $line->question_vid, $rid);
  }
  return $questions;
}

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

/**
 * Determine quiz availability status.
 * 
 * @return
 *  String representing status open, closed or future.
 */
function _quiz_availability($node) {
  $time = time();
  if ($node->quiz_always || $node->quiz_open < $time && $node->quiz_close > $time) {
    return 'open';
  }
  elseif ($node->quiz_open > $time) {
    return 'future';
  }
  return 'closed';
}

/**
 * Determine who should have access to the Take Quiz tab
 * depending on the quiz status
 */

/* UNUSED
function _quiz_status_access($node) {
  $status = _quiz_availability($node);
  switch ($status) {
    case 'closed':
    case 'future':
      return user_access('administer quiz');
    case 'open':
      return (user_access('access quiz') && $node->status);
  }
  return FALSE;
}
*/

/**
 * 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();
    $time = strtotime("+{$offset} days", $time);
  }

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

Functions

Namesort descending Description
quiz_access Implementation of hook_access().
quiz_build_question_list Retrieves a question list for a given quiz.
quiz_calculate_score Calculates the score user received on quiz.
quiz_delete Implementation of hook_delete().
quiz_end_actions Actions to take at the end of a quiz.
quiz_form Implementation of hook_form().
quiz_form_alter Implementation of hook_form_alter().
quiz_get_answers Get answers the user actually tried.
quiz_get_corrects Get an array of correct answer(s) for a question.
quiz_get_feedback Get feedback for one question.
quiz_get_number_of_questions Finds out the number of questions for the quiz.
quiz_get_num_questions Gets the number questions of a given type for a quiz.
quiz_get_pass_rate Finds out the pass rate for the quiz.
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. A quiz is keyed by nid/vid, so you need both. 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…
quiz_load Implementation of hook_load().
quiz_menu Implementation of hook_menu().
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_start_actions Actions to take place at the start of a quiz.
quiz_store_question_result Store a quiz question result.
quiz_take_quiz Handles quiz taking.
quiz_theme Implementation of hook_theme().
quiz_type_access_load Load a quiz node and validate it.
quiz_update Implementation of hook_update().
quiz_update_questions Updates the status of questions assigned to the quiz. Possible statuses are 'random', 'always', 'never'.
quiz_update_quiz_question_relationship Updates quiz-question relation entries in the quiz_node_relationship table.
quiz_validate Implementation of hook_validate().
quiz_view Implementation of hook_view().
_quiz_availability Determine quiz availability status.
_quiz_form_prepare_date Takes a time element and prepares to send it to form_date().
_quiz_get_answers
_quiz_get_feedback_options Get an array of feedback options.
_quiz_get_questions Retrieve list of published questions assigned to quiz.
_quiz_get_question_types Retrieve list of question types.
_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
_quiz_get_random_taxonomy_question_ids Given a term ID, get all of the question nid/vids that have that ID.
_quiz_get_results
_quiz_get_summary_text Get the summary message for a completed quiz.
_quiz_get_unused_questions Retrieve list of published questions not assigned to quiz.
_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_pick_result_option Get summary text for a particular score from a set of result options.
_quiz_taxonomy_select Prints a taxonomy selection form for each vocabulary.
_quiz_update_resultoptions Modify result of option-specific updates.

Constants