You are here

quiz.module in Quiz 5.2

File

quiz.module
View source
<?php

/**
 * @file
 * Quiz Module
 *
 * This module allows the creation of interactive quizzes for site visitors.
 */
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);

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

/**
 * 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_perm().
 */
function quiz_perm() {
  return array(
    QUIZ_PERM_ADMIN_CONFIG,
    'access quiz',
    'create quiz',
    'administer quiz',
    'user results',
  );
}

/**
 * Implementation of hook_access().
 */
function quiz_access($op, $node) {
  global $user;
  if ($op == 'view') {
    return user_access('access quiz');
  }
  if ($op == 'create') {
    return user_access('create quiz');
  }
  if ($op == 'update' || $op == 'delete') {
    if (user_access('create quiz') && $user->uid == $node->uid) {
      return TRUE;
    }
  }
  if (user_access('administer quiz')) {
    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_menu().
 */
function quiz_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $access = user_access(QUIZ_PERM_ADMIN_CONFIG);
    $items[] = array(
      'path' => 'admin/settings/quiz',
      'title' => t('@quiz Configuration', array(
        '@quiz' => QUIZ_NAME,
      )),
      'description' => t('Configure @quiz options.', array(
        '@quiz' => QUIZ_NAME,
      )),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'quiz_admin_settings',
      ),
      'access' => user_access(QUIZ_PERM_ADMIN_CONFIG),
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'node/add/quiz',
      'title' => t('@quiz', array(
        '@quiz' => QUIZ_NAME,
      )),
      'access' => user_access('create quiz'),
    );
    $items[] = array(
      'path' => 'admin/quiz/results',
      'title' => t('@quiz Results', array(
        '@quiz' => QUIZ_NAME,
      )),
      'callback' => 'quiz_admin',
      'access' => user_access('administer quiz'),
      'type' => MENU_NORMAL_ITEM,
    );
  }
  else {
    drupal_add_css(drupal_get_path('module', 'quiz') . '/quiz.css', 'module', 'all');
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(arg(1));
      if ($node->type == 'quiz') {

        // Menu item for creating adding questions to quiz.
        $items[] = array(
          'path' => 'node/' . arg(1) . '/questions',
          'title' => t('Manage questions'),
          'callback' => 'quiz_questions',
          'access' => user_access('create quiz'),
          'type' => MENU_LOCAL_TASK,
        );
        if (user_access('administer quiz')) {
          $items[] = array(
            'path' => 'node/' . arg(1) . '/quiz/admin',
            'title' => t('Quiz Admin', array(
              '@quiz' => QUIZ_NAME,
            )),
            'callback' => 'theme_quiz_view',
            'callback arguments' => array(
              node_load(arg(1)),
            ),
            'access' => user_access('administer quiz'),
            'type' => MENU_LOCAL_TASK,
          );
        }
      }
    }
    else {
      $items[] = array(
        'path' => 'user/' . arg(1) . '/myresults',
        'title' => t('My Results'),
        'callback' => 'quiz_get_user_results',
        'access' => user_access('user results'),
        'type' => MENU_LOCAL_TASK,
      );
      $items[] = array(
        'path' => 'user/quiz/' . arg(2) . '/userresults',
        'title' => t('User Results'),
        'callback' => 'quiz_user_results',
        'access' => user_access('user results'),
        'type' => MENU_CALLBACK,
      );
      $items[] = array(
        'path' => 'admin/quiz/' . arg(2) . '/view',
        'title' => t('View @quiz', array(
          '@quiz' => QUIZ_NAME,
        )),
        'callback' => 'quiz_admin_results',
        'access' => user_access('administer quiz'),
        'type' => MENU_CALLBACK,
      );
      $items[] = array(
        'path' => 'admin/quiz/' . arg(2) . '/delete',
        'title' => t('Delete @quiz', array(
          '@quiz' => QUIZ_NAME,
        )),
        'callback' => 'quiz_admin_result_delete',
        'access' => user_access('administer quiz'),
        'type' => MENU_CALLBACK,
      );
    }
  }
  return $items;
}

/**
 * Implementation of hook_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."),
  );
  $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;
}

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

/**
 * 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) {
  return db_result(db_query("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", $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));
}

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

/**
 * Updates quiz-question relation entires 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) " . "SELECT src.parent_nid, %d, src.child_nid, src.child_vid, src.question_status " . "FROM {quiz_node_relationship} AS src " . "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);
}

/**
 * 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} " . "(vid, nid, number_of_random_questions, shuffle, backwards_navigation, quiz_open, quiz_close, takes, pass_rate, summary_pass, summary_default, quiz_always, feedback_time, tid) " . "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);
}

/**
 * Insert call specific to result options.
 *
 * @param $node
 *  The quiz node.
 */
function _quiz_insert_resultoptions($node) {
  foreach ($node->resultoptions as $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) {
  if (empty($option['option_id'])) {
    $option['option_id'] = db_next_id('{quiz_node_result_options}' . '_option_id');
    $sql = "INSERT INTO {quiz_node_result_options} (nid, vid, option_id, option_name, option_summary, option_start, option_end)" . " VALUES(%d, %d, %d, '%s', '%s', %d, %d)";
    $values = array(
      $option['nid'],
      $option['vid'],
      $option['option_id'],
      $option['option_name'],
      $option['option_summary'],
      $option['option_start'],
      $option['option_end'],
    );
    db_query($sql, $values);
  }
}

/**
 * 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 = "\n      UPDATE {quiz_node_properties} SET\n        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      WHERE\n        vid = %d AND\n        nid = %d\n    ";
    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->vid, $node->nid);
  }
  _quiz_update_resultoptions($node);
}

/**
 * Result of option-specific updates.
 *
 * @param $node
 *  The quiz node.
 */
function _quiz_update_resultoptions($node) {
  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} SET option_name='%s', option_summary='%s', option_start = %d, " . " option_end = %d 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);
    }
  }
}

/**
 * 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));
  $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();
  }
  else {
    $node = node_prepare($node, $teaser);
  }
  return $node;
}

/**
 * Theme a message about the quiz's availability for quiz takers.
 * 
 * @ingroup themeable
 */
function theme_quiz_availability($node) {
  $status = _quiz_availability($node);
  $output = '<div class="quiz_availability"><p>';
  switch ($status) {
    case 'future':
      $output .= t('This quiz will not be available until %time.', array(
        '%time' => format_date($node->quiz_open),
      ));
      break;
    case 'open':
      $output .= t('This quiz closes %time.', array(
        '%time' => format_date($node->quiz_close),
      ));
      break;
    case 'closed':
      $output .= t('This quiz is no longer available.');
      break;
  }
  $output .= '</p></div>' . "\n";
  return $output;
}

/**
 * Theme the node view for quizzes.
 * 
 * @ingroup themeable
 */
function theme_quiz_view($node, $teaser = FALSE, $page = FALSE) {
  $output = '';

  // Output quiz options.
  $output .= '<h3>' . t('@quiz Options', array(
    '@quiz' => QUIZ_NAME,
  )) . '</h3>';
  $header = array(
    t('# of Random Questions'),
    t('Shuffle?'),
    t('Feedback'),
    t('Number of takes'),
  );
  $shuffle = $node->shuffle == 1 ? t('Yes') : t('No');
  $feedback_options = _quiz_get_feedback_options();
  $feedback = $feedback_options[$node->feedback_time];
  $takes = $node->takes == 0 ? t('Unlimited') : check_plain($node->takes);
  $rows = array();
  $rows[] = array(
    check_plain($node->number_of_random_questions),
    $shuffle,
    $feedback,
    $takes,
  );
  $output .= theme('table', $header, $rows);

  // Format Quiz Dates.
  $output .= '<h3>' . t('@quiz start/end', array(
    '@quiz' => QUIZ_NAME,
  )) . '</h3>';
  if (!$node->quiz_always) {

    // If we are previewing, make sure the dates are timestamps and not form arrays.
    if (is_array($node->quiz_open)) {
      quiz_translate_form_date($node, 'quiz_open');
    }
    if (is_array($node->quiz_close)) {
      quiz_translate_form_date($node, 'quiz_close');
    }

    // Format the availability info.
    $output .= '<p>' . format_date($node->quiz_open) . ' &mdash; ' . format_date($node->quiz_close) . '</p>';
    $output .= '<p><strong>' . t('Days @quiz live for: ', array(
      '@quiz' => QUIZ_NAME,
    )) . '</strong> ' . floor(($node->quiz_close - $node->quiz_open) / 60 / 60 / 24) . '</p>';
    $remaining = floor(($node->quiz_close - time()) / 60 / 60 / 24);
    $remaining = $remaining < 0 ? 'Expired' : $remaining;
    $output .= '<p><strong>Days remaining:</strong> ' . $remaining . '</p>';
    $elapsed = floor((time() - $node->quiz_open) / 60 / 60 / 24);
    $elapsed = $elapsed < 0 ? -$elapsed . ' days to go' : $elapsed;
    $output .= '<p><strong>Days since start:</strong> ' . $elapsed . '</p>';
  }
  else {
    $output .= '<p>' . t('This Quiz is always available.') . '</p>' . "\n";
  }

  // Format taxonomy selection (if applicable).
  if (function_exists(taxonomy_node_get_terms)) {
    $output .= '<h3>' . t('Taxonomy selection') . '</h3>';
    $terms = array();
    foreach (taxonomy_node_get_terms($node->nid) as $term) {
      $terms[] = check_plain($term->name);
    }
    if (!empty($terms)) {
      $terms = implode(', ', $terms);
      $output .= "<p>{$terms}</p>";
    }
    else {
      $output .= '<p>' . t('No selected terms found') . '</p>';
    }
  }

  // Format pass / fail and summary options.
  if ($node->pass_rate || $node->summary_default || $node->summary_pass) {
    if ($node->pass_rate) {
      $output .= '<h3>' . t('Pass / fail and summary options') . '</h3>' . "\n";
      $output .= '<p><strong>' . t('Percentage needed to pass:') . '</strong> ' . check_plain($node->pass_rate) . '</p>' . "\n";
      $output .= '<div><strong>' . t('Summary text if the user passed:') . '</strong> ';
      $output .= $node->summary_pass ? check_markup($node->summary_pass) : t('No text defined.');
      $output .= '</div>' . "\n";
    }
    $output .= '<div><strong>' . t('Default summary text:') . '</strong> ';
    $output .= $node->summary_default ? check_markup($node->summary_default) : t('No text defined.');
    $output .= '</div>' . "\n";
  }

  // Format result options if available.
  if (count($node->resultoptions)) {
    $scored_quiz = $node->pass_rate > 0;
    $output .= '<h3>' . t('!quiz Results', array(
      '!quiz' => QUIZ_NAME,
    )) . '</h3>';
    $header = array(
      t('Name') => 'option_name',
      t('Summary') => 'option_summary',
    );
    if ($scored_quiz) {
      $header = array_merge($header, array(
        t('Start') => 'option_start',
        t('End') => 'option_end',
      ));
    }
    $values = array_values($header);
    foreach ($node->resultoptions as $option) {
      $row = array();
      foreach ($values as $field) {
        $row[] = $option[$field];
      }
      $option_rows[] = $row;
    }
    $output .= theme('table', array_keys($header), $option_rows);
  }

  // Format quiz questions.
  if (is_numeric(arg(1))) {
    $output .= '<h3>' . t('@quiz Questions', array(
      '@quiz' => QUIZ_NAME,
    )) . '</h3>';
    $questions = _quiz_get_questions($node->vid);
    $output .= theme('quiz_question_table', $questions, $node->nid);
  }
  return $output;
}

/**
 * Displays all the quizzes the user has taken part in.
 *
 * @return
 *  HTML output for page.
 */
function quiz_get_user_results() {
  global $user;
  $results = array();
  $dbresult = db_query("SELECT\n                          n.nid as nid,\n                          n.title as title,\n                          u.name as name,\n                          qnrs.result_id as result_id,\n                          qnrs.time_start,\n                          qnrs.time_end\n                        FROM {node} n, {quiz_node_properties} qnp, {quiz_node_results} qnrs, {users} u\n                        WHERE\n                          n.type = 'quiz'\n                            AND\n                          n.nid = qnp.nid\n                            AND\n                          qnrs.nid = qnp.nid\n                            AND\n                          u.uid = qnrs.uid\n                            AND\n                          u.uid = " . $user->uid . "\n                        ORDER BY qnrs.result_id ASC");

  // Create an array out of the results.
  while ($line = db_fetch_array($dbresult)) {
    $results[$line['result_id']] = $line;
  }
  return theme('quiz_get_user_results', $results);
}

/**
 * Handles quiz taking.
 *
 * @return
 *  HTML output for page.
 */
function quiz_take_quiz() {
  global $user;
  if (arg(0) == 'node' && is_numeric(arg(1)) && user_access('access quiz')) {
    if ($quiz = node_load(arg(1))) {

      // 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 . '/quiz/start/' . md5(mt_rand() . time()));
      }
      if ($user->uid && $quiz->takes != 0) {
        $times = db_num_rows(db_query("SELECT result_id " . "FROM {quiz_node_results} " . "WHERE nid = %d AND vid = %d AND uid = %d", $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 '';
        }
      }
      if ($_POST['op'] == t('Back')) {
        unset($_POST['tries']);
        array_unshift($_SESSION['quiz_' . $quiz->nid]['quiz_questions'], $_SESSION['quiz_' . $quiz->nid]['previous_quiz_questions']);
      }

      // 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']);
          $_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'],
          ));
          $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']);
        return $output;
      }
      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;
      }
    }
  }

  // If we got this far, the quiz does not exist.
  drupal_not_found();
}

/**
 * Store a quiz question result.
 */
function quiz_store_question_result($nid, $vid, $rid, $is_correct) {
  $result = db_query("SELECT result_id FROM {quiz_node_results_answers} WHERE question_nid = %d AND question_vid = %d AND result_id = %d", $nid, $vid, $rid);
  if (db_num_rows($result)) {
    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 {
    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;
}

/**
 * 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 {
      if (trim($quiz->summary_pass) != '') {
        $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));
}

/**
 * Actions to take place at the start of a quiz.
 *
 * @param $uid
 *  User ID
 * @param $nid
 *  Quiz node ID
 * @return integer
 *  Returns quiz_node_results result_id, or false if there is an error.
 */
function quiz_start_actions($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).
    if (gmmktime() >= $quiz->quiz_close || gmmktime() < $quiz->quiz_open) {
      drupal_set_message(t('This @quiz is not currently available.', array(
        '@quiz' => QUIZ_NAME,
      )), 'status');
      if (!user_access('create quiz')) {
        return FALSE;
      }
    }
  }

  // Get the results.
  global $user;

  //$results = _quiz_get_results($quiz->nid, $user->uid);

  // Check to see if the user alredy passed this quiz,
  // but only perform this check if it is a registered user.
  if ($user->uid) {
    $passed = db_result(db_query("SELECT result_id " . "FROM {quiz_node_results} qnrs " . "INNER JOIN {quiz_node_properties} USING (vid, nid) " . "WHERE qnrs.vid = %d AND qnrs.nid = %d AND qnrs.uid =%d " . "AND score >= pass_rate", $quiz->vid, $quiz->nid, $user->uid));
    if ($passed) {
      drupal_set_message(t('You have already passed this @quiz.', array(
        '@quiz' => QUIZ_NAME,
      )), 'status');
    }
  }

  // Insert quiz_node_results record.
  $rid = db_next_id('{quiz_node_results}_result_id');
  $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 $rid;
  }
  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));
    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;
}
function _quiz_get_random_questions($num_random, $tid) {
  $questions = array();
  if ($num_random > 0) {
    if ($tid > 0) {

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

/**
 * Implementation of hook_help().
 */
function quiz_help($section) {
  switch ($section) {
    case 'admin/help#quiz':
      return t('
<h3>Description</h3>
<p>The quiz module allows users to administer a quiz, as a sequence of questions, and track the answers given. It allows for the creation of questions (and their answers), and organizes these questions into a quiz. Finally, it provides a mechanism for ensuring question quality through a combination of community revision and moderation. Its target audience includes educational institutions, online training programs, employers, and people who just want to add a fun activity for their visitors to their Drupal site.</p>
<h3>Creating Your First Quiz</h3>
<p>Creating an initial quiz requires three steps:</p>
<ol>
  <li>Create at least one taxonomy vocabulary and assign it to the quiz and question type modules</li>
  <li>Create a series of questions</li>
  <li>Create a quiz based on the series of questions</li>
</ol>
    <h4>Setting Quiz Permissions</h4>
    <p>The quiz module and included multichoice module both have a plethora of permission options.<br />Unless you take care setting your permissions, the quiz module might not do everything you want it to do.</p>
    <h5><strong><a href="@admin-access#module-quiz">Quiz Permissions</a></strong></h5>
    <dl><dt><strong>access quiz</strong></dt>
    <dd>allows users to take quizzes</dd>
    <dt><strong>administer quiz</strong></dt>
    <dd>allows users to edit quizzes</dd>
    <dt><strong>administer quiz configuration</strong></dt>
    <dd>allows users to use global admin settings, as well as override userresults if they don\'t have that permission. Can also delete quizzes</dd>
    <dt><strong>create quiz</strong></dt>
    <dd>Can create a new quiz</dd>
    <dt><strong>user results</strong></dt>
    <dd>Can view other user\'s results.</dd></dl>
    <h5><strong><a href="@admin-access#module-multichoice">Multichoice Permissions</a></strong></h5>
    <dl><dt><strong>allow any number of answers</strong></dt>
    <dd>can submit questions with more than one correct answer.</dd>
    <dt><strong>allow feedback</strong></dt>
    <dd>Can create feedback when creating a new multichoice question.</dd>
    <dt><strong>allow multiple correct answers</strong></dt>
    <dd></dd>
    <dt><strong>allow user titles</strong></dt>
    <dd>Allows users to pick a name for their questions. Otherwise this is auto generated. The question name is never seen on the @quiz.</dd>
    <dt><strong>create multichoice</strong></dt>
    <dd>users can create multichoice questions</dd>
    <dt><strong>edit own multichoice</strong></dt>
    <dd>can edit their own multi-choice questions.</dd></dl>
<h4>Setting up a vocabulary</h4>
<ol>
  <li>If not already enabled, go to the <a href="@admin-modules">Administer >> Site building >> Modules</a> section of the control panel and check the <strong>enable</strong> checkbox to enable the <strong>taxonomy module</strong>.</li>
  <li>If you do not already have a taxonomy <strong>vocabulary</strong> suitable for quizzes, go to <a href="@admin-taxonomy">Administer >> Content management >> Categories</a> and create a vocabulary for quizzes (for example, <strong>Quiz Topics</strong>). Ensure that under <strong>Types</strong>, both <strong>quiz</strong> and all question types (for example, <strong>multichoice</strong>) are selected. Depending on your needs, you may wish to create a hierarchical vocabulary, so that topics can be sub-divided into smaller areas, and/or enable multiple select to associate quizzes and questions with more than one category.</li>
  <li>Add a series of <strong>terms</strong> to the vocabulary to which questions and quizzes can be assigned. For example:
    <ul>
      <li>Literature
        <ul>
          <li>Children\'s books</li>
          <li>Poetry</li>
          <li>Shakespeare</li>
        </ul>
      </li>
      <li>Mathematics
        <ul>
          <li>Algebra</li>
          <li>Trigonometry</li>
          <li>Calculus</li>
        </ul>
      </li>
      <li>Information Technology
        <ul>
          <li>Hardware</li>
          <li>Programming</li>
          <li>Databases</li>
        </ul>
      </li>
  </li>
</ol>
<h4>Creating quiz questions</h4>
<ol>
  <li>Begin by clicking <a href="@create-content">Create content</a>, and then select a question type node (for example, <a href="@multichoice">multichoice</a>)</li>
  <li>Fill out the question form. The presented interface will vary depending on the question type, but for multiple choice questions:
    <dl>
      <dt><strong>Title</strong></dt>
      <dd>Question title. This will be displayed as the heading of the question.</dd>
      <dt><strong>Taxonomy selection</strong></dt>
      <dd>Any taxonomy vocabularies that are assigned to the question type will be displayed.</dd>
      <dt><strong>Question</strong></dt>
      <dd>The actual question text (for example, <strong>What is 2+2?</strong>).</dd>
      <dt><strong>Multiple Answers</strong></dt>
      <dd>Whether or not the question has multiple correct answers, such as a "Select all that apply" question.</dd>
      <dt><strong>Correct</strong></dt>
      <dd>Indicates that given answer is a correct answer.</dd>
      <dt><strong>Answer</strong></dt>
      <dd>An answer choice (for example, <strong>4</strong>). If more answers are required, check <strong>I need more answers</strong> and click the <b>Preview</b> button.</dd>
      <dt><strong>Feedback</strong></dt>
      <dd>Feedback, if supplied, will be provided to the user at the end of the quiz.</dd>
    </dl>
  </li>
  <li>Repeat for each question you would like included on the quiz.</li>
</ol>
<h4>Creating the quiz</h4>
<ol>
  <li>Go to <a href="@create-quiz">Create content >> Quiz</a> to access the quiz creation form.</li>
  <li>Fill out the form to set the @quiz options:
    <dl>
      <dt><strong>Title</strong></dt>
      <dd>Quiz title. This will be displayed as the heading of the quiz.</dd>
      <dt><strong>Taxonomy selection</strong></dt>
      <dd>Any taxonomy vocabularies that are assigned to the quiz type will be displayed. Select from the terms displayed in order to assign the quiz to vocabulary terms.</dd>
      <dt><strong>Shuffle questions</strong></dt>
      <dd>Whether or not to shuffle (randomize) the questions.</dd>
      <dt><strong>Number of takes</strong></dt>
      <dd>Number of takes to allow user. Varies from 1-9 or Unlimited times.</dd>
    </dl>
  </li>
  <li>Once the quiz has been created, click the <b>add questions</b> tab to assign questions to the quiz.</li>
  <li>Select a radio button next to each question indicating if the question should appear (Randomly, Always, or Never), and click <strong>Submit questions</strong>.</li>
  <li>Repeat process until satisfied with question selection.</li>
</ol>
    ', array(
        '@quiz' => QUIZ_NAME,
        '@admin-access' => url('admin/user/access'),
        '@admin-modules' => url('admin/build/modules'),
        '@admin-taxonomy' => url('admin/content/taxonomy'),
        '@create-content' => url('node/add'),
        '@multichoice' => url('node/add/multichoice'),
        '@create-quiz' => url('node/add/quiz'),
      ));
    case 'node/add#quiz':
      return t('A collection of questions designed to create interactive tests');
    default:
      break;
  }
}

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

/**
 * Handles "Manage questions" tab.
 *
 * Displays form which allows questions to be assigned to the given quiz.
 *
 * @return
 *  HTML output to create page.
 */
function quiz_questions_form() {
  $quiz = node_load(arg(1));

  // Set page title.
  drupal_set_title(check_plain($quiz->title));

  // Display links to create other questions.
  $form['additional_questions'] = array(
    '#type' => 'fieldset',
    '#title' => t('Create additional questions'),
    '#theme' => 'additional_questions',
  );
  foreach (_quiz_get_question_types() as $type) {
    $form['additional_questions'][$type] = array(
      '#type' => 'markup',
      '#value' => l(t($type), 'node/add/' . $type . '/' . $quiz->nid, array(
        'title' => t('Go to ' . $type . ' administration'),
      )) . ' ',
    );
  }

  // Display questions 'always' on this quiz.
  $form['filtered_question_list_always'] = array(
    '#type' => 'fieldset',
    '#title' => t("Questions 'always' on this quiz"),
    '#theme' => 'quiz_filtered_questions',
    '#collapsible' => TRUE,
    'question_status' => array(
      '#tree' => TRUE,
    ),
  );
  $form['filtered_random_question_list']['num_random_questions'] = array(
    '#type' => 'textfield',
    '#title' => t('Number questions to randomize'),
    '#size' => 3,
    '#default_value' => $quiz->number_of_random_questions,
    '#description' => t('The number of randomly selected questions to assign to this quiz.'),
  );
  $form['filtered_random_question_list']['random_term_id'] = array(
    '#type' => 'select',
    '#title' => t('Terms'),
    '#size' => 1,
    '#options' => _quiz_taxonomy_select($quiz->tid),
    '#default_value' => $quiz->tid,
    '#description' => t('Randomly select from questions with this term, or choose from the random question pool below'),
  );

  // Display questions 'random' on this quiz.
  $form['filtered_question_list_random'] = array(
    '#type' => 'fieldset',
    '#title' => t("Questions 'random' on this quiz"),
    '#theme' => 'quiz_filtered_questions',
    '#collapsible' => TRUE,
    'question_status' => array(
      '#tree' => TRUE,
    ),
  );

  // Display filtered question list.
  $form['filtered_question_list'] = array(
    '#type' => 'fieldset',
    '#title' => t("Questions 'never' on this quiz"),
    '#theme' => 'quiz_filtered_questions',
    '#collapsible' => TRUE,
    'question_status' => array(
      '#tree' => TRUE,
    ),
  );

  // Get all questions and their status in relation to this quiz.
  $questions = array_merge(_quiz_get_unused_questions($quiz->vid), _quiz_get_questions($quiz->vid));
  foreach ($questions as $question) {
    switch ($question->status) {
      case QUESTION_RANDOM:
        $_form =& $form['filtered_question_list_random'];
        break;
      case QUESTION_ALWAYS:
        $_form =& $form['filtered_question_list_always'];
        break;
      case QUESTION_NEVER:
        $_form =& $form['filtered_question_list'];
        break;
    }
    $_form['question_status'][$question->nid] = array(
      '#type' => 'radios',
      '#options' => array(
        QUESTION_RANDOM => '',
        QUESTION_ALWAYS => '',
        QUESTION_NEVER => '',
      ),
      '#default_value' => $question->status,
    );
    $_form['question'][$question->nid] = array(
      '#type' => 'markup',
      '#value' => $question->question,
    );
    $_form['type'][$question->nid] = array(
      '#type' => 'markup',
      '#value' => $question->type,
    );
  }

  // Show the number of 'always' questions in the 'always' table header.
  $form['filtered_question_list_always']['#title'] .= ' (' . count($form['filtered_question_list_always']['type']) . ')';
  $form['new_revision'] = array(
    '#type' => 'checkbox',
    '#default_value' => in_array('revision', variable_get('node_options_quiz', array())),
    '#title' => t('New Revision'),
    '#description' => t('Allow question status changes to create a new revision of the quiz?'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit questions'),
  );
  $form['timestamp'] = array(
    '#type' => 'hidden',
    '#value' => time(),
  );
  return $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 = node_load((int) arg(1));
  $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;
}
function quiz_questions() {
  return drupal_get_form('quiz_questions_form');
}

/**
 * Submit function for quiz_questions.
 * 
 * Updates from the "add questions" tab.
 * 
 * @param $form_id
 *  A string containing the form id.
 * @param $values
 *  Array containing the form values.
 */
function quiz_questions_form_submit($form_id, $values) {

  // Load the node.
  $quiz = node_load(arg(1));

  // Update quiz with selected question options.
  if (!quiz_update_questions($values['question_status'], $values['new_revision'])) {
    form_set_error('', t('Either no questions were selected, or there was a problem updating your @quiz. Please try again.', array(
      '@quiz' => QUIZ_NAME,
    )));
    return;
  }

  // Check if selecting random question from pool, and not via term.
  if (empty($values['random_term_id'])) {
    $assigned_random = 0;
    if (is_array($values['question_status'])) {
      foreach ($values['question_status'] as $id => $status) {
        if (QUESTION_ALWAYS == $status) {
          $assigned_random++;
        }
      }
    }
    if ($values['num_random_questions'] > $assigned_random) {
      $values['num_random_questions'] = $assigned_random;
      drupal_set_message(t('The number of random questions for this @quiz have been lowered to %anum to match the number of questions you assigned.', array(
        '@quiz' => QUIZ_NAME,
        '%anum' => $assigned_random,
      ), 'status'));
    }
  }
  else {

    // Warn user if not enough questions available with this term_id.
    $available_random = count(_quiz_get_random_questions($values['num_random_questions'], $values['random_term_id']));
    if ($values['num_random_questions'] > $available_random) {
      drupal_set_message(t('There are currently not enough questions assigned to this term (@random). Please lower the number of random quetions or assign more questions to this taxonomy term before taking this @quiz.', array(
        '@random' => $available_random,
        '@quiz' => QUIZ_NAME,
      )), 'error');
    }
  }
  $result = db_query("UPDATE {quiz_node_properties} SET number_of_random_questions = %d, tid = %d WHERE vid = %d AND nid = %d", $values['num_random_questions'], $values['random_term_id'], $quiz->vid, $quiz->nid);
  if (!$result) {
    drupal_set_message(t('There was an error updating the @quiz.', array(
      '@quiz' => QUIZ_NAME,
    )), 'error');
  }
  else {
    drupal_set_message(t('Questions updated successfully.'));
  }
}

/**
 * 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_num_rows(db_query("SELECT 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 $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($submitted_questions, $revision = FALSE) {

  // Load quiz node.
  $quiz = node_load(arg(1));
  $return = true;

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

  // Get array of all questions with a valid (enabled) question type (in database).
  $questions_in_db = _quiz_get_unused_questions($quiz->vid, TRUE) + _quiz_get_questions($quiz->vid, TRUE, TRUE);

  // Create an array of question statuses (in database), keyed by question vid.
  $status_in_db = array();
  foreach ($questions_in_db as $question) {
    $status_in_db[$question->nid] = $question->status;
  }

  // Determine which questions have had their status changed;
  // e.g. unassigned questions assigned to either 'always' or 'random', etc.
  $changes = array_diff_assoc($submitted_questions, $status_in_db);

  // Create new revision of current quiz (if any question status has been changed).
  if (!empty($changes)) {
    drupal_execute('node_form', array(
      'revision' => '1',
    ), $quiz);
    drupal_set_message(t('A new revision of the @quiz has been created.', array(
      '@quiz' => QUIZ_NAME,
    )));
  }

  // Separate all questions that have had their status changed
  //  into corresponding arrays (inserts or updates).
  $inserts = array();
  $updates = array();
  foreach ($changes as $nid => $status) {
    if ($status_in_db[$nid] == QUESTION_NEVER) {
      $inserts[$nid] = $status;
    }
    else {
      $updates[$nid] = $status;
    }
  }

  // Insert question(s) into quiz; status will be either 'always' or 'random'.
  foreach ($inserts as $nid => $status) {
    $child_vid = $questions_in_db[$nid]->vid;
    $sql = "INSERT INTO {quiz_node_relationship} (parent_nid, parent_vid, child_nid, child_vid, question_status) " . "SELECT src.nid, src.vid, %d, %d, %d FROM {node} AS src WHERE src.nid = %d";
    $result = db_query($sql, $nid, $child_vid, $status, $quiz->nid);
    if (!$result) {
      return FALSE;
    }
  }

  // Update question status in quiz; i.e. change status from 'always' or 'random' to another status.
  foreach ($updates as $nid => $status) {
    $child_vid = $questions_in_db[$nid]->vid;
    $sql = "UPDATE {quiz_node_relationship} SET question_status = %d " . "WHERE parent_vid = (SELECT src.vid FROM {node} AS src WHERE src.nid = %d) AND parent_nid = %d AND child_vid = %d";
    $result = db_query($sql, $status, $quiz->nid, $quiz->nid, $child_vid);
    if (!$result) {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Implementation of hook_settings().
 */
function quiz_admin_settings() {
  $form = array();

  // Option to globally turn off pass/fail form elements.
  $form['quiz_default_close'] = array(
    '#type' => 'textfield',
    '#title' => t('Default number of days before a @quiz is closed', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#default_value' => variable_get('quiz_default_close', 30),
    '#description' => t('Supply a number of days to calculate the default close date for new quizzes.'),
  );
  $form['quiz_use_passfail'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display pass/fail options in the @quiz form', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#default_value' => variable_get('quiz_use_passfail', 1),
    '#description' => t('Check this to display the pass/fail options in the @quiz form. If you want to prohibit other quiz creators from changing the default pass/fail percentage set below, uncheck this option.', array(
      '@quiz' => QUIZ_NAME,
    )),
  );
  $form['quiz_default_pass_rate'] = array(
    '#type' => 'textfield',
    '#title' => t('Default percentage needed to pass a @quiz', array(
      '@quiz' => QUIZ_NAME,
    )),
    '#default_value' => variable_get('quiz_default_pass_rate', 75),
    '#description' => t('Supply a number between 1 and 100 to set as the default percentage correct needed to pass a quiz. Set to 0 if you want to ignore pass/fail summary information by default.'),
  );
  $form['quiz_name'] = array(
    '#type' => 'textfield',
    '#title' => t('Assessment name'),
    '#default_value' => QUIZ_NAME,
    '#description' => t('How do you want to refer to quizzes across the site (for example: quiz, test, assessment).  This will affect display text but will not affect menu paths.'),
    '#required' => TRUE,
  );
  return system_settings_form($form);
}

/**
 * Validation of the Form Settings form.
 */
function quiz_settings_form_validate($form_id, $form_values) {
  if (!is_numeric($form_values['num_random_questions']) || $form_values['num_random_questions'] < 0) {
    form_set_error('num_random_questions', t('The number of random questions must be at least 0.'));
  }
  if (!is_numeric($form_values['quiz_default_close']) || $form_values['quiz_default_close'] <= 0) {
    form_set_error('quiz_default_close', t('The default number of days before a quiz is closed must be a number greater than 0.'));
  }
  if (!is_numeric($form_values['quiz_default_pass_rate'])) {
    form_set_error('quiz_default_pass_rate', t('The pass rate value must be a number between 0% and 100%.'));
  }
  if ($form_values['quiz_default_pass_rate'] > 100) {
    form_set_error('quiz_default_pass_rate', t('The pass rate value must not be more than 100%.'));
  }
  if ($form_values['quiz_default_pass_rate'] < 0) {
    form_set_error('quiz_default_pass_rate', t('The pass rate value must not be less than 0%.'));
  }
}

/**
 * Quiz Admin.
 */
function quiz_admin() {
  $results = _quiz_get_results();
  return theme('quiz_admin', $results);
}

/*
 * Get a full results list.
 */
function _quiz_get_results($nid = '', $uid = 0) {
  $results = array();
  $args = array();
  $sql = "SELECT n.nid as nid,\n                 n.title as title,\n                 u.name as name,\n                 qnrs.result_id as result_id,\n                 qnrs.time_start,\n                 qnrs.time_end\n          FROM {node} n, {quiz_node_properties} qnp, {quiz_node_results} qnrs, {users} u\n          WHERE\n            n.type = 'quiz'\n              AND\n            n.nid = qnp.nid\n              AND\n            qnrs.nid = qnp.nid\n              AND\n            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;
}

/*
 * Quiz Results User.
 */
function quiz_user_results() {
  $result = db_fetch_object(db_query('SELECT qnp.nid FROM {quiz_node_properties} qnp, {quiz_node_results} qnrs WHERE qnrs.nid = qnp.nid AND qnrs.result_id = %d', arg(2)));
  if ($result->nid) {
    $quiz = node_load($result->nid);
    $questions = _quiz_get_answers(arg(2));
    $score = quiz_calculate_score($quiz, arg(2));
    $summary = _quiz_get_summary_text($quiz, $score);
    return theme('quiz_user_summary', $quiz, $questions, $score, $summary);
  }
  else {
    drupal_not_found();
  }
}

/*
 * Quiz Results Admin.
 */
function quiz_admin_results() {
  $result = db_fetch_object(db_query('SELECT qnp.nid FROM {quiz_node_properties} qnp, {quiz_node_results} qnrs WHERE qnrs.nid = qnp.nid AND qnrs.result_id = %d', arg(2)));
  if ($result->nid) {
    $quiz = node_load($result->nid);
    $questions = _quiz_get_answers(arg(2));
    $score = quiz_calculate_score($quiz, arg(2));
    $summary = _quiz_get_summary_text($quiz, $score);
    return theme('quiz_admin_summary', $quiz, $questions, $score, $summary);
  }
  else {
    drupal_not_found();
  }
}

/*
 * Delete Result.
 */
function quiz_admin_result_delete() {
  return drupal_get_form('quiz_admin_result_delete_form');
}
function quiz_admin_result_delete_form() {
  $form['del_rid'] = array(
    '#type' => 'hidden',
    '#value' => arg(2),
  );
  return confirm_form($form, t('Are you sure you want to delete this @quiz result?', array(
    '@quiz' => QUIZ_NAME,
  )), 'admin/quiz/results', t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}
function quiz_admin_result_delete_form_submit($form_id, $form_values) {
  db_query("DELETE FROM {quiz_node_results} WHERE result_id = %d", $form_values['del_rid']);
  db_query("DELETE FROM {quiz_node_results_answers} WHERE result_id = %d", $form_values['del_rid']);
  drupal_set_message(t('Deleted result.'));
  return "admin/quiz/results";
}
function _quiz_get_answers($rid) {
  $questions = array();
  $ids = db_query("SELECT question_nid, question_vid, type FROM {quiz_node_results_answers} " . "LEFT JOIN {node} ON (question_nid = nid AND question_vid = vid) WHERE result_id = %d ORDER BY answer_timestamp", $rid);
  while ($line = db_fetch_object($ids)) {
    $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) {
  if (!$node->quiz_always) {

    // The quiz is currently open.
    if ($node->quiz_open > time()) {
      $status = 'future';
    }
    elseif ($node->quiz_open < time() && $node->quiz_close > time()) {
      $status = 'open';
    }
    else {
      $status = 'closed';
    }
  }
  else {
    $status = 'open';
  }
  return $status;
}

/**
 * Determine who should have access to the Take Quiz tab
 * depending on the quiz status
 */
function _quiz_status_access($node) {
  $access = FALSE;
  $status = _quiz_availability($node);
  switch ($status) {
    case 'closed':
    case 'future':
      $access = user_access('administer quiz');
      break;
    case 'open':
      $access = user_access('access quiz') && $node->status;
      break;
  }
  return $access;
}

/**
 *
 *
 * THEME FUNCTIONS!
 *
 *
 */

/**
 * Theme the admin results table.
 * 
 * @param $results
 *  As returned by _quiz_get_results().
 * 
 * @ingroup themeable
 */
function theme_quiz_admin($results) {
  $output = '';
  $rows = array();
  while (list($key, $result) = each($results)) {
    $rows[] = array(
      l('view', 'admin/quiz/' . $result['result_id'] . '/view') . ' | ' . l('delete', 'admin/quiz/' . $result['result_id'] . '/delete'),
      check_plain($result['title']),
      check_plain($result['name']),
      $result['result_id'],
      format_date($result['time_start'], 'small'),
      $result['time_end'] > 0 ? format_date($result['time_end'], 'small') : t('In Progress'),
    );
  }
  $header = array(
    t('Action'),
    t('@quiz Title', array(
      '@quiz' => QUIZ_NAME,
    )),
    t('Username'),
    t('Result<br />ID'),
    t('Time Started'),
    t('Finished?'),
  );
  if (!empty($rows)) {
    $output .= theme('table', $header, $rows);
  }
  else {
    $output .= t('No @quiz results found.', array(
      '@quiz' => QUIZ_NAME,
    ));
  }
  return $output;
}

/**
 * Theme the user results page.
 * 
 * @param $results
 *  An array of quiz information.
 * @return
 *  Themed html.
 * 
 * @ingroup themeable
 */
function theme_quiz_get_user_results($results) {
  $output = '';
  $rows = array();
  while (list($key, $result) = each($results)) {
    $rows[] = array(
      l('view', 'user/quiz/' . $result['result_id'] . '/userresults'),
      check_plain($result['title']),
      check_plain($result['name']),
      $result['result_id'],
      format_date($result['time_start'], 'small'),
      $result['time_end'] > 0 ? format_date($result['time_end'], 'small') : t('In Progress'),
    );
  }
  $header = array(
    t('Action'),
    t('@quiz Title', array(
      '@quiz' => QUIZ_NAME,
    )),
    t('Username'),
    t('Result<br />ID'),
    t('Time Started'),
    t('Finished?'),
  );
  if (!empty($rows)) {
    $output .= theme('table', $header, $rows);
  }
  else {
    $output .= t('No @quiz results found.', array(
      '@quiz' => QUIZ_NAME,
    ));
  }
  return $output;
}

/**
 * Theme the filtered question list.
 * 
 * @ingroup themeable
 */
function theme_quiz_filtered_questions($form) {
  $quiz_id = is_numeric(arg(1)) ? arg(1) : NULL;
  $header = array(
    t('Random'),
    t('Always'),
    t('Never'),
    t('Question'),
    t('Type'),
    t('Edit'),
  );
  $rows = array();
  while (list($nid, $values) = each($form['question_status'])) {
    if (is_numeric($nid)) {
      $rows[] = array(
        drupal_render($form['question_status'][$nid][QUIZ_FEEDBACK_END]),
        drupal_render($form['question_status'][$nid][QUIZ_FEEDBACK_QUESTION]),
        drupal_render($form['question_status'][$nid][QUIZ_FEEDBACK_NEVER]),
        drupal_render($form['question'][$nid]),
        drupal_render($form['type'][$nid]),
        l(t('Edit'), 'node/' . $nid . '/edit/' . $quiz_id),
      );
    }
  }
  if (!empty($rows)) {
    $output .= theme('table', $header, $rows);
  }
  else {
    $output .= t('No questions found.');
  }
  return $output;
}

/**
 * Theme a table containing array of questions and options.
 *
 * @param $questions
 *  Array of question nodes.
 * @return
 *  HTML for a table.
 * 
 * @ingroup themeable
 */
function theme_quiz_question_table($questions, $quiz_id = NULL) {
  $output = '';
  $rows = array();
  $status_descriptions = array(
    t('Random'),
    t('Always'),
    t('Never'),
  );
  while (list($key, $question) = each($questions)) {
    $rows[] = array(
      $status_descriptions[$question->status],
      $question->question,
      $question->type,
      l(t('Edit'), 'node/' . $question->nid . '/edit/' . $quiz_id),
    );
  }
  $header = array(
    t('Status'),
    t('Question'),
    t('Type'),
    t('Edit'),
  );
  if (!empty($rows)) {
    $output .= theme('table', $header, $rows);
  }
  else {
    $output .= t('No questions found.');
  }
  return $output;
}

/**
 * Pass the correct mark to the theme so that theme authors can use an image.
 * 
 * @ingroup themeable
 */
function theme_quiz_score_correct() {
  return theme('image', drupal_get_path('module', 'quiz') . '/images/correct.gif', t('correct'));
}

/**
 * Pass the incorrect mark to the theme so that theme authors can use an image.
 *
 * @ingroup themeable
 */
function theme_quiz_score_incorrect() {
  return theme('image', drupal_get_path('module', 'quiz') . '/images/incorrect.gif', t('incorrect'));
}

/**
 * Theme a progress indicator for use during a quiz.
 * 
 * @param $question_number
 *  The position of the current question in the sessions' array.
 * @param $num_of_question
 *  The number of questions for this quiz as returned by quiz_get_number_of_questions().
 * @return
 *  Themed html.
 * 
 * @ingroup themeable
 */
function theme_quiz_progress($question_number, $num_of_question) {

  // Determine the percentage finished (not used, but left here for other implementations).

  //$progress = ($question_number*100)/$num_of_question;

  // Get the current question # by adding one.
  $current_question = $question_number + 1;
  $output = '';
  $output .= '<div id="quiz_progress">';
  $output .= t('Question %x of %y', array(
    '%x' => $current_question,
    '%y' => $num_of_question,
  ));
  $output .= '</div><br />' . "\n";
  return $output;
}

/**
 * Theme a question page.
 * 
 * @param $quiz
 *  The quiz node object.
 * @param $question_node
 *  The question node.
 * @return
 *  Themed html.
 * 
 * @ingroup themeable
 */
function theme_quiz_take_question($quiz, $question_node) {

  //Calculation for quiz progress bar.
  $number_of_questions = quiz_get_number_of_questions($quiz->vid, $quiz->nid);
  $question_number = $number_of_questions - count($_SESSION['quiz_' . $quiz->nid]['quiz_questions']);
  $question_node->question_number = $question_number;

  // Set the title here in case themers want to do something different.
  drupal_set_title(check_plain($quiz->title));

  // Return the elements of the page.
  $output = '';
  $output .= theme('quiz_progress', $question_number, $number_of_questions);
  $output .= module_invoke($question_node->type, 'render_question', $question_node);
  return $output;
}

/**
 * Theme the summary page after the quiz has been completed.
 * 
 * @param $quiz
 *  The quiz node object.
 * @param $questions
 *  The questions array as defined by _quiz_get_answers.
 * @param $score
 *  Array of score information as returned by quiz_calculate_score().
 * @param $summary
 *  Filtered text of the summary.
 * @return
 *  Themed html.
 * 
 * @ingroup themeable
 */
function theme_quiz_take_summary($quiz, $questions, $score, $summary) {

  // Set the title here so themers can adjust.
  drupal_set_title(check_plain($quiz->title));

  // Display overall result.
  $output = '';
  if ($score['percentage_score']) {
    $output .= '<div id="quiz_score_possible">' . t('You got %num_correct of %question_count correct.', array(
      '%num_correct' => $score['num_correct'],
      '%question_count' => $score['question_count'],
    )) . '</div>' . "\n";
    $output .= '<div id="quiz_score_percent">' . t('Your score: %score%', array(
      '%score' => $score['percentage_score'],
    )) . '</div><br />' . "\n";
  }
  $output .= '<div id="quiz_summary">' . $summary . '</div><br />' . "\n";

  // Get the feedback for all questions.
  $output .= theme('quiz_feedback', $questions, $quiz->pass_rate > 0, TRUE);
  return $output;
}

/**
 * Theme the summary page for admins.
 * 
 * @param $quiz
 *  The quiz node object.
 * @param $questions
 *  The questions array as defined by _quiz_get_answers.
 * @param $score
 *  Array of score information as returned by quiz_calculate_score().
 * @param $summary
 *  Filtered text of the summary.
 * @return
 *  Themed html.
 * 
 * @ingroup themeable
 */
function theme_quiz_admin_summary($quiz, $questions, $score, $summary) {

  // Set the title here so themers can adjust.
  drupal_set_title(check_plain($quiz->title));

  // Display overall result.
  $output = '';
  $output .= '<div id="quiz_score_possible">' . t('This person got %num_correct of %question_count correct.', array(
    '%num_correct' => $score['num_correct'],
    '%question_count' => $score['question_count'],
  )) . '</div>' . "\n";
  $output .= '<div id="quiz_score_percent">' . t('Total score: @score%', array(
    '@score' => $score['percentage_score'],
  )) . '</div><br />' . "\n";
  $output .= '<div id="quiz_summary">' . $summary . '</div><br />' . "\n";

  // Get the feedback for all questions.
  $output .= theme('quiz_feedback', $questions, TRUE, TRUE);
  return $output;
}

/**
 * Theme the summary page for user results.
 * 
 * @param $quiz
 *  The quiz node object.
 * @param $questions
 *  The questions array as defined by _quiz_get_answers.
 * @param $score
 *  Array of score information as returned by quiz_calculate_score().
 * @param $summary
 *  Filtered text of the summary.
 * @return
 *  Themed html.
 * 
 * @ingroup themeable
 */
function theme_quiz_user_summary($quiz, $questions, $score, $summary) {

  // Set the title here so themers can adjust.
  drupal_set_title(check_plain($quiz->title));

  // Display overall result.
  $output = '';
  $output .= '<div id="quiz_score_possible">' . t('You got %num_correct of %question_count correct.', array(
    '%num_correct' => $score['num_correct'],
    '%question_count' => $score['question_count'],
  )) . '</div>' . "\n";
  $output .= '<div id="quiz_score_percent">' . t('Your score was: @score%', array(
    '@score' => $score['percentage_score'],
  )) . '</div><br />' . "\n";
  $output .= '<div id="quiz_summary">' . $summary . '</div><br />' . "\n";

  // Get the feedback for all questions.
  $output .= theme('quiz_feedback', $questions, FALSE, TRUE);
  return $output;
}

/**
 * Theme the question feedback.
 * 
 * @param $questions
 *  Array of quiz objects as returned by _quiz_get_answers.
 * @param showpoints
 *  Binary flag for whether to show the actual answers or not.
 * @param $showfeedback
 *  Binary flag for whether to show question feedback.
 * @return
 *  Themed html.
 * 
 * @ingroup themeable
 */
function theme_quiz_feedback($questions, $showpoints = TRUE, $showfeedback = FALSE) {
  $header = array(
    t('Question Result(s)'),
    '',
  );

  // Go through each of the questions.
  foreach ($questions as $question) {
    $cols = array();

    // Ask each question to render a themed report of how the user did.
    $cols[] = array(
      'data' => theme($question->type . '_report', $question, $showpoints, $showfeedback),
      'class' => 'quiz_summary_qrow',
    );

    // Get the score result for each question only if it's a scored quiz.
    if ($showpoints) {
      $theme = $question->correct ? 'quiz_score_correct' : 'quiz_score_incorrect';
      $cols[] = array(
        'data' => theme($theme),
        'class' => 'quiz_summary_qcell',
      );
    }

    // Pack all of this into a table row.
    $rows[] = array(
      'data' => $cols,
      'class' => 'quiz_summary_qrow',
    );
  }
  return theme('table', $header, $rows);
}

/**
 * Theme feedback for one question.
 *
 * @param $quiz
 *  Quiz node (may not be needed).
 * @param $question_node
 *  The question node giving feedback for.
 * @param $answer
 *  User's response to previous question.
 * @return
 *  Themed html.
 * 
 * @ingroup themeable
 */
function theme_quiz_question_feedback($quiz, $report) {
  $output = '<div class="quiz-summary-question">';
  $output .= theme($report->type . '_feedback', $quiz, $report);
  $output .= '</div><br class="clear" />';
  return $output;
}

/**
 * Allow the option to theme the questions form.
 * 
 * @ingroup themeable
 */
function theme_quiz_questions($form) {
  $output = '';
  $output .= drupal_render($form);
  return $output;
}

/**
 * Theme the "no feedback" option.
 *
 * @return
 *  Themed html feedback.
 * 
 * @ingroup themeable
 */
function theme_quiz_no_feedback() {
  return t('Thanks for taking the quiz!');
}

Functions

Namesort descending Description
quiz_access Implementation of hook_access().
quiz_admin Quiz Admin.
quiz_admin_results
quiz_admin_result_delete
quiz_admin_result_delete_form
quiz_admin_result_delete_form_submit
quiz_admin_settings Implementation of hook_settings().
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_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_get_user_results Displays all the quizzes the user has taken part in.
quiz_help Implementation of hook_help().
quiz_insert Implementation of hook_insert().
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_questions
quiz_questions_form Handles "Manage questions" tab.
quiz_questions_form_submit Submit function for quiz_questions.
quiz_settings_form_validate Validation of the Form Settings form.
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_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 entires in the quiz_node_relationship table.
quiz_user_results
quiz_validate Implementation of hook_validate().
quiz_view Implementation of hook_view().
theme_quiz_admin Theme the admin results table.
theme_quiz_admin_summary Theme the summary page for admins.
theme_quiz_availability Theme a message about the quiz's availability for quiz takers.
theme_quiz_feedback Theme the question feedback.
theme_quiz_filtered_questions Theme the filtered question list.
theme_quiz_get_user_results Theme the user results page.
theme_quiz_no_feedback Theme the "no feedback" option.
theme_quiz_progress Theme a progress indicator for use during a quiz.
theme_quiz_questions Allow the option to theme the questions form.
theme_quiz_question_feedback Theme feedback for one question.
theme_quiz_question_table Theme a table containing array of questions and options.
theme_quiz_score_correct Pass the correct mark to the theme so that theme authors can use an image.
theme_quiz_score_incorrect Pass the incorrect mark to the theme so that theme authors can use an image.
theme_quiz_take_question Theme a question page.
theme_quiz_take_summary Theme the summary page after the quiz has been completed.
theme_quiz_user_summary Theme the summary page for user results.
theme_quiz_view Theme the node view for quizzes.
_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_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_status_access Determine who should have access to the Take Quiz tab depending on the quiz status
_quiz_taxonomy_select Prints a taxonomy selection form for each vocabulary.
_quiz_update_resultoptions Result of option-specific updates.

Constants